+Отображение открытых паков
// ==UserScript==
// @name Remanga
// @namespace http://tampermonkey.net/
// @version 8.0.0
// @description +Отображение открытых паков
// @author You
// @license MIT
// @match https://remanga.org/*
// @match https://xn--80aaig9ahr.xn--c1avg/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=remanga.org
// @connect xn--80aaig9ahr.xn--c1avg
// @connect api.xn--80aaig9ahr.xn--c1avg
// @connect remanga.org
// @connect api.remanga.org
// @connect update.greatest.deepsurf.us
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const IS_MIRROR = location.hostname === 'xn--80aaig9ahr.xn--c1avg';
const SITE_DOMAIN = IS_MIRROR ? 'https://xn--80aaig9ahr.xn--c1avg' : 'https://remanga.org';
const API_DOMAIN = IS_MIRROR ? 'https://api.xn--80aaig9ahr.xn--c1avg' : 'https://api.remanga.org';
const MY_ID_KEY = 'rem_user_id_v1';
const MY_INV_KEY = 'rem_inventory_covers_v1';
const MY_WISH_KEY = 'rem_wish_covers_v1';
const STORAGE_FIX_KEY = 'rem_script_fix_active';
const SETTINGS_KEYS = {
onlineStatus: 'rem_set_online_status',
ownBadge: 'rem_set_own_badge',
cardStats: 'rem_set_card_stats',
profileStats: 'rem_set_profile_stats',
fixMode: 'rem_set_fix_mode',
customBgEnabled: 'rem_set_bg_enabled',
customBgOpacity: 'rem_set_bg_opacity',
customBgBlur: 'rem_set_bg_blur',
customBgFit: 'rem_set_bg_fit',
themeAccent: 'rem_set_theme_accent',
themeButtonBg: 'rem_set_theme_btn_bg',
themeNameColor: 'rem_set_theme_name_color',
themeNameFont: 'rem_set_theme_name_font',
themeMenuBg: 'rem_set_theme_menu_bg',
themeSiteBg: 'rem_set_theme_site_bg',
scanButton: 'rem_set_scan_btn',
deckStats: 'rem_set_deck_stats',
wishBadge: 'rem_set_wish_badge',
};
const BG_URL_LS_KEY = 'rem_bg_url_data';
const DEFAULTS = {
onlineStatus: true,
ownBadge: true,
cardStats: true,
profileStats: true,
fixMode: false,
customBgEnabled: false,
customBgOpacity: 80,
customBgBlur: 0,
customBgFit: 'cover',
themeAccent: '',
themeButtonBg: '',
themeNameColor: '',
themeNameFont: '',
themeMenuBg: '',
themeSiteBg: '',
scanButton: true,
deckStats: false,
wishBadge: true,
};
const THEME_PRESETS = [
{ name: 'По умолчанию', value: '' },
{ name: '🔵 Синий', value: '#3b82f6' },
{ name: '🟣 Фиолетовый', value: '#a855f7' },
{ name: '🟢 Зелёный', value: '#22c55e' },
{ name: '🔴 Красный', value: '#ef4444' },
{ name: '🟠 Оранжевый', value: '#f97316' },
{ name: '🌸 Розовый', value: '#ec4899' },
{ name: '🌊 Бирюзовый', value: '#06b6d4' },
{ name: '🌟 Золотой', value: '#eab308' },
];
const cfg = {};
function loadSettings() {
for (const [k, dflt] of Object.entries(DEFAULTS)) {
try {
cfg[k] = (typeof GM_getValue !== 'undefined') ? GM_getValue(SETTINGS_KEYS[k], dflt) : JSON.parse(localStorage.getItem(SETTINGS_KEYS[k]) ?? JSON.stringify(dflt));
} catch (e) {
cfg[k] = dflt;
}
}
cfg.customBgUrl = localStorage.getItem(BG_URL_LS_KEY) || '';
}
function saveSetting(key, value) {
cfg[key] = value;
if (key === 'customBgUrl') {
try { localStorage.setItem(BG_URL_LS_KEY, value); } catch (e) { console.warn('REM BG: localStorage переполнен'); }
} else {
try {
if (typeof GM_setValue !== 'undefined') GM_setValue(SETTINGS_KEYS[key], value);
else localStorage.setItem(SETTINGS_KEYS[key], JSON.stringify(value));
} catch (e) { }
}
}
loadSettings();
async function checkForUpdates() {
const URL = 'https://update.greatest.deepsurf.us/scripts/570944/Remanga.meta.js';
const current = (typeof GM_info !== 'undefined') ? GM_info.script.version : '7.2.5';
Manager.showStatus('Проверка обновлений...', true);
try {
const resp = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({ method: 'GET', url: URL + '?t=' + Date.now(), onload: resolve, onerror: reject });
});
const m = resp.responseText.match(/@version\s+([\d.]+)/);
if (m) {
const latest = m[1];
if (latest !== current) {
if (confirm(`Доступна новая версия: ${latest} (у вас ${current})\n\nПерейти на страницу обновления?`)) {
window.location.href = 'https://update.greatest.deepsurf.us/scripts/570944/Remanga.user.js';
}
} else { alert('У вас установлена актуальная версия.'); }
}
} catch (e) { alert('Ошибка при проверке обновлений.'); }
finally { Manager.showStatus('', false); }
}
let activeObserver = null;
const ExState = {
scanning: false,
scanDone: 0,
scanTotal: 0,
results: [],
currentRank: 'ALL',
page: 0,
perPage: 8,
lastSource: '',
searchQuery: '',
searchUser: '',
};
const SCAN_LIST_KEY = 'rem_scan_list_v1';
function getScanList() {
try { return JSON.parse(localStorage.getItem(SCAN_LIST_KEY) || '[]'); } catch { return []; }
}
function saveScanList(list) {
localStorage.setItem(SCAN_LIST_KEY, JSON.stringify(list));
}
function addToScanList(id, username) {
const list = getScanList();
id = String(id);
if (list.find(u => String(u.id) === id)) return;
list.push({ id, username: username || `User_${id}` });
saveScanList(list);
}
function removeFromScanList(id) {
const list = getScanList().filter(u => String(u.id) !== String(id));
saveScanList(list);
}
function isInScanList(id) {
return getScanList().some(u => String(u.id) === String(id));
}
const SCAN_HISTORY_KEY = 'rem_scan_history_v2';
function getHistory() {
let hist = localStorage.getItem(SCAN_HISTORY_KEY);
if (!hist) {
const old = localStorage.getItem('rem_scan_history_v1');
if (old) {
localStorage.setItem(SCAN_HISTORY_KEY, old);
hist = old;
}
}
try { return JSON.parse(hist || '[]'); } catch { return []; }
}
function saveToHistory(name, results, link = "") {
let hist = getHistory();
const exIdx = hist.findIndex(h => h.name === name || (link && h.link === link));
let id;
if (exIdx > -1) {
id = hist[exIdx].id;
hist[exIdx].time = Date.now();
hist[exIdx].count = results.length;
if (link) hist[exIdx].link = link;
const it = hist.splice(exIdx, 1)[0];
hist.unshift(it);
} else {
id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
hist.unshift({ id, name, link, time: Date.now(), count: results.length });
}
if (hist.length > 20) {
const old = hist.pop();
try { if (typeof GM_deleteValue !== 'undefined') GM_deleteValue('rem_scan_data_' + old.id); } catch (e) { }
localStorage.removeItem('rem_scan_data_' + old.id);
}
try {
localStorage.setItem(SCAN_HISTORY_KEY, JSON.stringify(hist));
if (typeof GM_setValue !== 'undefined') GM_setValue('rem_scan_data_' + id, JSON.stringify(results));
else localStorage.setItem('rem_scan_data_' + id, JSON.stringify(results));
} catch (e) {
console.error('History save error:', e);
try { localStorage.setItem('rem_scan_data_' + id, JSON.stringify(results)); } catch (e2) { }
}
return id;
}
function loadFromHistory(id) {
try {
let raw = null;
if (typeof GM_getValue !== 'undefined') raw = GM_getValue('rem_scan_data_' + id);
if (!raw) {
raw = localStorage.getItem('rem_scan_data_' + id);
}
if (!raw) {
console.warn('REM: Данные сканирования не найдены для', id);
return null;
}
return JSON.parse(raw);
} catch (e) {
console.error('REM: Ошибка парсинга истории', e);
return null;
}
}
function deleteFromHistory(id) {
const hist = getHistory().filter(h => h.id !== id);
localStorage.setItem(SCAN_HISTORY_KEY, JSON.stringify(hist));
try { if (typeof GM_deleteValue !== 'undefined') GM_deleteValue('rem_scan_data_' + id); } catch (e) { }
localStorage.removeItem('rem_scan_data_' + id);
}
function captureVideoFrame(videoUrl, container, seekTime = 1) {
const video = document.createElement('video');
video.crossOrigin = 'anonymous';
video.muted = true;
video.preload = 'auto';
video.src = videoUrl;
video.addEventListener('loadeddata', () => {
video.currentTime = Math.min(seekTime, video.duration || seekTime);
});
video.addEventListener('seeked', () => {
try {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth || 120;
canvas.height = video.videoHeight || 180;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const img = document.createElement('img');
img.src = canvas.toDataURL('image/webp', 0.8);
img.style.cssText = 'width:100%;height:100%;object-fit:cover;';
container.innerHTML = '';
container.appendChild(img);
} catch (e) { /* CORS */ }
video.remove();
});
video.addEventListener('error', () => { video.remove(); });
}
function isVideoUrl(url) {
if (!url) return false;
return /\.(webm|mp4)([?#]|$)/i.test(url);
}
function renderCardMedia(fullImg, container) {
if (!fullImg) return;
if (isVideoUrl(fullImg)) {
captureVideoFrame(fullImg, container);
}
}
const RANK_ORDER = ['rank_f', 'rank_e', 'rank_d', 'rank_c', 'rank_b', 'rank_a', 'rank_s', 'rank_re'];
const RANK_MAP = {
'rank_f': { name: 'F', color: '#9ca3af' },
'rank_e': { name: 'E', color: '#9ca3af' },
'rank_d': { name: 'D', color: '#ffffff' },
'rank_c': { name: 'C', color: '#4ade80' },
'rank_b': { name: 'B', color: '#60a5fa' },
'rank_a': { name: 'A', color: '#c084fc' },
'rank_s': { name: 'S', color: '#fbbf24' },
'rank_re': { name: 'RE', color: '#f59e0b' },
};
const STYLES = `
.rem-online-dot {
position:absolute!important;top:2px!important;right:2px!important;
width:11px;height:11px;border-radius:50%;border:2px solid #000;
z-index:101;pointer-events:none;
}
.rem-online-dot.online{background-color:#22c55e!important;}
.rem-online-dot.offline{background-color:#ef4444!important;}
.rem-own-badge {
position:absolute!important;bottom:5px!important;left:5px!important;z-index:50!important;
width:22px;height:22px;border-radius:50%;
background-color:#22c55e;color:#fff;
display:flex;align-items:center;justify-content:center;
border:2px solid #fff;pointer-events:none;
opacity:0;animation:remFadeIn .3s forwards;box-shadow:none!important;
}
.rem-own-badge svg{width:14px;height:14px;stroke-width:3;}
.rem-wish-badge {
position:absolute!important;bottom:5px!important;right:5px!important;z-index:50!important;
width:22px;height:22px;border-radius:50%;
background-color:#3b82f6;color:#fff;
display:flex;align-items:center;justify-content:center;
border:2px solid #fff;pointer-events:none;
opacity:0;animation:remFadeIn .3s forwards;box-shadow:none!important;
}
.rem-wish-badge svg{width:14px;height:14px;stroke-width:3;}
@keyframes remFadeIn{from{opacity:0;transform:scale(.5)}to{opacity:1;transform:scale(1)}}
.rem-toggle {
display:inline-flex;align-items:center;justify-content:center;
margin-left:10px;padding:2px 8px;border-radius:6px;
font-size:11px;font-weight:800;text-transform:uppercase;
cursor:pointer;border:1px solid #3f3f46;color:#fff;height:22px;vertical-align:middle;
transition:all .2s;
}
.rem-toggle.on{background-color:#15803d;border-color:#22c55e;}
.rem-toggle.off{background-color:transparent;color:#71717a;border-color:#3f3f46;}
.rem-toggle:hover{filter:brightness(1.2);}
#rem-loader-status {
position:fixed;bottom:20px;left:70px;z-index:999999;
background:#18181b;color:#fff;padding:8px 16px;border-radius:20px;
border:1px solid #27272a;font-size:13px;font-weight:500;
box-shadow:0 4px 15px rgba(0,0,0,.6);display:none;align-items:center;gap:10px;
}
#rem-loader-status.active{display:flex;animation:slideIn .3s;}
@keyframes slideIn{from{opacity:0;transform:translateX(-20px)}to{opacity:1;transform:translateX(0)}}
html.rem-checking body>*:not(#rem-loader){display:none!important;}
#rem-loader{position:fixed;inset:0;background:#0c0c0c;z-index:999999;display:flex;align-items:center;justify-content:center;color:#666;font-family:sans-serif;}
body.rem-active main>*:not(#rem-inject){display:none!important;}
#rem-inject{width:100%;max-width:650px;margin:0 auto;min-height:80vh;font-family:system-ui,-apple-system,sans-serif;padding:10px;}
.rem-grid{display:grid;grid-template-columns:repeat(4,1fr)!important;gap:10px;padding-bottom:40px;}
.rem-card{position:relative;aspect-ratio:2/3;background:#18181b;border:1px solid #27272a;border-radius:6px;overflow:hidden;cursor:pointer;transition:.2s;display:flex;}
.rem-card:hover{border-color:#71717a;}
.rem-card img{width:100%;height:100%;object-fit:cover;}
.rem-fix-header{margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid #27272a;}
.rem-fix-title{font-size:24px;font-weight:700;color:#fff;margin:0 0 4px 0;}
.rem-fix-meta{color:#a1a1aa;font-size:13px;display:flex;align-items:center;margin-top:8px;}
.rem-sep{margin:0 8px;opacity:.5;}
.rem-over{position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px);animation:F .2s;}
.rem-box{position:relative;width:90%;max-width:450px;background:#0c0c0c;border:1px solid #27272a;border-radius:16px;padding:24px;display:flex;flex-direction:column;gap:20px;animation:Z .2s;}
@keyframes F{from{opacity:0}to{opacity:1}} @keyframes Z{from{transform:scale(.95)}to{transform:scale(1)}}
.rem-box-img{width:180px;aspect-ratio:2/3;margin:10px auto 0;border-radius:12px;overflow:hidden;border:1px solid #27272a;}
.rem-box-img img{width:100%;height:100%;object-fit:cover;}
.rem-info{text-align:center;display:flex;flex-direction:column;gap:4px;}
.rem-lnk-m{color:#fff;font-size:14px;font-weight:500;text-decoration:none;opacity:.9;display:inline-block;}
.rem-lnk-c{color:#fff;font-size:20px;font-weight:700;text-decoration:none;display:inline-block;}
.rem-acts{display:flex;justify-content:center;gap:12px;margin-top:5px;}
.rem-pill{height:36px;padding:0 16px;border-radius:99px;background:#27272a;color:#fff;border:none;display:flex;align-items:center;gap:6px;font-size:14px;font-weight:500;}
.rem-sub{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin-top:5px;}
.rem-s-btn{height:36px;padding:0 16px;border-radius:99px;background:#27272a;color:#fff;text-decoration:none;font-size:14px;font-weight:500;display:flex;align-items:center;}
.rem-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;background:#27272a;color:#fff;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;}
#rem-cfg-btn {
position:fixed;bottom:20px;left:20px;z-index:999999;
width:40px;height:40px;border-radius:50%;
background:#18181b;border:1px solid #27272a;color:#a1a1aa;
display:flex;align-items:center;justify-content:center;
cursor:pointer;transition:.2s;box-shadow:0 4px 10px rgba(0,0,0,.5);
font-size:18px;
}
#rem-cfg-btn:hover{color:#fff;border-color:#71717a;transform:rotate(45deg);}
#rem-settings-panel {
position:fixed;bottom:70px;left:20px;z-index:999998;
width:300px;background:#18181b;border:1px solid #27272a;
border-radius:16px;padding:0;overflow:hidden;
box-shadow:0 8px 32px rgba(0,0,0,.7);
transform:scale(.95) translateY(10px);opacity:0;
transition:transform .2s, opacity .2s;
pointer-events:none;
}
#rem-settings-panel.open {
transform:scale(1) translateY(0);opacity:1;pointer-events:all;
}
.rem-panel-header {
padding:14px 16px 12px;border-bottom:1px solid #27272a;
display:flex;align-items:center;justify-content:space-between;
}
.rem-panel-title {
font-size:15px;font-weight:700;color:#fff;display:flex;align-items:center;gap:8px;
}
.rem-panel-title span.badge {
font-size:10px;background:#27272a;color:#71717a;
padding:2px 6px;border-radius:4px;font-weight:600;
}
.rem-panel-body { padding:8px 0; max-height:70vh; overflow-y:auto; scrollbar-width:thin; scrollbar-color:#3f3f46 transparent; }
.rem-panel-body::-webkit-scrollbar { width:4px; }
.rem-panel-body::-webkit-scrollbar-thumb { background:#3f3f46; border-radius:2px; }
.rem-section-label {
font-size:10px;font-weight:700;color:#52525b;text-transform:uppercase;
letter-spacing:.08em;padding:8px 16px 4px;
}
.rem-row {
display:flex;align-items:center;justify-content:space-between;
padding:9px 16px;cursor:default;transition:background .15s;
gap:10px;
}
.rem-row:hover{ background:rgba(255,255,255,.04); }
.rem-row-info { display:flex;flex-direction:column;gap:1px;flex:1;min-width:0; }
.rem-row-name { font-size:13px;color:#e4e4e7;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
.rem-row-desc { font-size:11px;color:#71717a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
.rem-sw { position:relative;width:36px;height:20px;flex-shrink:0;cursor:pointer; }
.rem-sw input { opacity:0;width:0;height:0;position:absolute; }
.rem-sw-track {
position:absolute;inset:0;border-radius:10px;background:#3f3f46;
transition:background .2s;
}
.rem-sw input:checked + .rem-sw-track { background:#16a34a; }
.rem-sw-thumb {
position:absolute;width:14px;height:14px;border-radius:50%;background:#fff;
top:3px;left:3px;transition:left .2s;pointer-events:none;
}
.rem-sw input:checked ~ .rem-sw-thumb { left:19px; }
.rem-sub-row{padding:4px 16px 8px;display:flex;flex-direction:column;gap:4px;}
.rem-sub-row-label{font-size:10px;color:#52525b;font-weight:600;}
.rem-url-input{width:100%;height:30px;border-radius:6px;border:1px solid #27272a;background:#18181b;color:#e4e4e7;font-size:11px;padding:0 8px;outline:none;transition:.15s;}
.rem-url-input:focus{border-color:#3b82f6;}
.rem-url-input::placeholder{color:#3f3f46;}
.rem-ex-cloud{margin-bottom:16px;}
.rem-ex-cloud-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#18181b;border:1px solid #1e1e21;border-radius:8px;transition:.15s;cursor:pointer;}
.rem-ex-cloud-item:hover{border-color:#3b82f6;background:rgba(59,130,246,.06);}
.rem-ex-cloud-item-left{display:flex;align-items:center;gap:8px;min-width:0;}
.rem-ex-cloud-item-name{font-size:13px;color:#e4e4e7;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.rem-ex-cloud-item-meta{font-size:11px;color:#52525b;white-space:nowrap;}
.rem-ex-cloud-item-badge{font-size:9px;padding:2px 6px;border-radius:4px;background:rgba(96,165,250,.15);color:#60a5fa;font-weight:700;flex-shrink:0;}
.rem-panel-footer {
border-top:1px solid #27272a;padding:10px 16px;
display:flex;gap:8px;flex-wrap:wrap;
}
.rem-action-btn {
flex:1;min-width:0;height:32px;border-radius:8px;border:1px solid #3f3f46;
background:transparent;color:#a1a1aa;font-size:11px;font-weight:600;
cursor:pointer;transition:all .15s;white-space:nowrap;
padding:0 4px;overflow:hidden;text-overflow:ellipsis;
}
.rem-action-btn:hover{ background:#27272a;color:#fff;border-color:#52525b; }
.rem-action-btn.danger:hover{ background:#450a0a;color:#f87171;border-color:#7f1d1d; }
.rem-profile-stat-badge {
margin-top:6px;display:inline-flex;align-items:center;justify-content:center;gap:6px;
background-color:#18181b;border:1px solid #27272a;padding:4px 10px;border-radius:6px;
color:#a1a1aa;font-size:12px;font-weight:600;width:fit-content;align-self:center;
}
.rem-profile-stat-badge strong{color:#22c55e;margin-left:4px;}
.rem-counting strong{color:#eab308;}
.rem-progress-box{width:100%;margin:15px 0;background:#18181b;border:1px solid #27272a;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:8px;}
.rem-progress-header{display:flex;justify-content:space-between;align-items:center;color:#fff;font-size:14px;font-weight:600;}
.rem-progress-text span{color:#22c55e;}
.rem-progress-track{width:100%;height:8px;background:#27272a;border-radius:4px;overflow:hidden;margin-bottom:4px;}
.rem-progress-fill{height:100%;background:linear-gradient(90deg,#22c55e,#4ade80);border-radius:4px;width:0%;transition:width .5s ease-out;}
.rem-rank-stats{display:flex;flex-wrap:nowrap;overflow-x:auto;gap:6px;margin-top:8px;padding-bottom:2px;-webkit-overflow-scrolling:touch;scrollbar-width:none;}
.rem-rank-stats::-webkit-scrollbar{display:none;}
.rem-rank-badge{display:inline-flex;align-items:center;border-radius:4px;padding:2px 8px;font-size:11px;font-weight:700;border:1px solid;background:rgba(0,0,0,.3);flex-shrink:0;white-space:nowrap;}
#rem-inject{width:100%;max-width:650px;margin:0 auto;min-height:80vh;font-family:system-ui,-apple-system,sans-serif;padding:10px;}
.rem-grid{display:grid;grid-template-columns:repeat(4,1fr)!important;gap:10px;padding-bottom:40px;}
.rem-card{position:relative;aspect-ratio:2/3;background:#18181b;border:1px solid #27272a;border-radius:6px;overflow:hidden;cursor:pointer;transition:.2s;display:flex;}
.rem-card:hover{border-color:#71717a;}
.rem-card img{width:100%;height:100%;object-fit:cover;}
.rem-fix-header{margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid #27272a;}
.rem-fix-title{font-size:24px;font-weight:700;color:#fff;margin:0 0 4px 0;}
.rem-fix-meta{color:#a1a1aa;font-size:13px;display:flex;align-items:center;margin-top:8px;}
.rem-sep{margin:0 8px;opacity:.5;}
.rem-over{position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px);animation:F .2s;}
.rem-box{position:relative;width:90%;max-width:450px;background:#0c0c0c;border:1px solid #27272a;border-radius:16px;padding:24px;display:flex;flex-direction:column;gap:20px;animation:Z .2s;}
@keyframes F{from{opacity:0}to{opacity:1}} @keyframes Z{from{transform:scale(.95)}to{transform:scale(1)}}
.rem-box-img{width:180px;aspect-ratio:2/3;margin:10px auto 0;border-radius:12px;overflow:hidden;border:1px solid #27272a;}
.rem-box-img img{width:100%;height:100%;object-fit:cover;}
.rem-info{text-align:center;display:flex;flex-direction:column;gap:4px;}
.rem-lnk-m{color:#fff;font-size:14px;font-weight:500;text-decoration:none;opacity:.9;display:inline-block;}
.rem-lnk-c{color:#fff;font-size:20px;font-weight:700;text-decoration:none;display:inline-block;}
.rem-acts{display:flex;justify-content:center;gap:12px;margin-top:5px;}
.rem-pill{height:36px;padding:0 16px;border-radius:99px;background:#27272a;color:#fff;border:none;display:flex;align-items:center;gap:6px;font-size:14px;font-weight:500;}
.rem-sub{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin-top:5px;}
.rem-s-btn{height:36px;padding:0 16px;border-radius:99px;background:#27272a;color:#fff;text-decoration:none;font-size:14px;font-weight:500;display:flex;align-items:center;}
.rem-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;background:#27272a;color:#fff;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;}
.rem-card-stats-row{display:flex;gap:8px;margin-top:10px;justify-content:center;width:100%;flex-wrap:wrap;}
.rem-stat-bubble{background:#18181b;border:1px solid #27272a;border-radius:10px;padding:6px 10px;display:flex;flex-direction:column;align-items:center;min-width:75px;transition:.2s;}
.rem-stat-bubble:hover{border-color:#3f3f46;}
.rem-stat-num{font-size:16px;font-weight:800;line-height:1;}
.rem-stat-lab{font-size:9px;color:#71717a;text-transform:uppercase;margin-top:4px;font-weight:600;}
.rem-stat-num.loading{color:#3f3f46;}
.rem-stat-bubble.owners .rem-stat-num{color:#22c55e;}
.rem-stat-bubble.wants .rem-stat-num{color:#3b82f6;}
.rem-stat-bubble.sales .rem-stat-num{color:#f59e0b;}
#rem-exchange-panel{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:999998;width:680px;max-width:94vw;max-height:88vh;background:#0f0f11;border:1px solid #27272a;border-radius:20px;display:none;flex-direction:column;overflow:hidden;box-shadow:0 24px 80px rgba(0,0,0,.7),0 0 0 1px rgba(255,255,255,.03);animation:Z .25s;}
#rem-exchange-panel.open{display:flex;}
.rem-ex-backdrop{position:fixed;inset:0;z-index:999997;background:rgba(0,0,0,.6);backdrop-filter:blur(3px);display:none;}
.rem-ex-backdrop.open{display:block;}
.rem-ex-header{padding:20px 24px 16px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #1e1e21;}
.rem-ex-title{font-size:18px;font-weight:700;color:#fff;display:flex;align-items:center;gap:8px;}
.rem-ex-close{width:32px;height:32px;border-radius:50%;background:#1e1e21;border:1px solid #2e2e33;color:#a1a1aa;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:.2s;font-size:16px;}
.rem-ex-close:hover{background:#27272a;color:#fff;border-color:#3f3f46;}
.rem-ex-body{flex:1;overflow-y:auto;padding:16px 24px 24px;scrollbar-width:thin;scrollbar-color:#27272a transparent;}
.rem-ex-body::-webkit-scrollbar{width:6px;}
.rem-ex-body::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px;}
.rem-ex-input-row{display:flex;gap:8px;margin-bottom:16px;}
.rem-ex-input{flex:1;background:#18181b;border:1px solid #27272a;border-radius:10px;color:#e4e4e7;font-size:13px;padding:10px 14px;outline:none;transition:border .15s;}
.rem-ex-input:focus{border-color:#3b82f6;}
.rem-ex-input::placeholder{color:#52525b;}
.rem-ex-btn{padding:0 20px;border-radius:10px;border:none;font-size:13px;font-weight:600;cursor:pointer;transition:.15s;display:flex;align-items:center;gap:6px;}
.rem-ex-btn.primary{background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff;}
.rem-ex-btn.primary:hover{filter:brightness(1.1);transform:translateY(-1px);}
.rem-ex-btn.secondary{background:#1e1e21;color:#a1a1aa;border:1px solid #27272a;}
.rem-ex-btn.secondary:hover{background:#27272a;color:#fff;}
.rem-ex-btn:disabled{opacity:.5;cursor:default;transform:none!important;filter:none!important;}
.rem-ex-progress{width:100%;margin:12px 0 16px;text-align:center;}
.rem-ex-pbar-track{width:100%;height:6px;background:#1e1e21;border-radius:3px;overflow:hidden;margin:8px 0;}
.rem-ex-pbar-fill{height:100%;background:linear-gradient(90deg,#3b82f6,#60a5fa);border-radius:3px;width:0%;transition:width .3s;}
.rem-ex-ptext{font-size:12px;color:#71717a;}
.rem-ex-ptext strong{color:#a1a1aa;}
.rem-ex-rank-bar{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px;}
.rem-ex-rank-btn{padding:6px 14px;border-radius:8px;background:#18181b;border:1px solid #27272a;color:#a1a1aa;font-size:12px;font-weight:600;cursor:pointer;transition:.15s;}
.rem-ex-rank-btn:hover{border-color:#3f3f46;color:#fff;}
.rem-ex-rank-btn.active{background:#3b82f6;border-color:#3b82f6;color:#fff;}
.rem-ex-rank-count{font-size:10px;color:#71717a;margin-left:2px;}
.rem-ex-summary{background:#18181b;border:1px solid #27272a;border-radius:12px;padding:14px 18px;margin-bottom:16px;display:flex;gap:20px;justify-content:center;flex-wrap:wrap;}
.rem-ex-s-item{text-align:center;}
.rem-ex-s-num{font-size:22px;font-weight:800;line-height:1;}
.rem-ex-s-lab{font-size:10px;color:#71717a;text-transform:uppercase;font-weight:600;margin-top:2px;}
.rem-ex-results{display:flex;flex-direction:column;gap:10px;}
.rem-ex-card{background:#18181b;border:1px solid #27272a;border-radius:12px;overflow:hidden;transition:.15s;cursor:pointer;}
.rem-ex-card:hover{border-color:#3f3f46;transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.3);}
.rem-ex-card-inner{display:flex;gap:14px;padding:14px;}
.rem-ex-card-img{width:60px;height:90px;border-radius:8px;overflow:hidden;flex-shrink:0;background:#27272a;}
.rem-ex-card-img img{width:100%;height:100%;object-fit:cover;}
.rem-ex-card-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px;}
.rem-ex-card-name{font-size:14px;font-weight:700;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.rem-ex-card-rank{font-size:11px;font-weight:600;padding:2px 8px;border-radius:4px;display:inline-block;width:fit-content;}
.rem-ex-card-traders{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px;}
.rem-ex-card-tag{font-size:11px;padding:3px 8px;border-radius:6px;font-weight:600;display:flex;align-items:center;gap:4px;}
.rem-ex-card-tag.buyers{background:rgba(59,130,246,.15);color:#60a5fa;}
.rem-ex-card-tag.sellers{background:rgba(249,115,22,.15);color:#fb923c;}
.rem-ex-card-tag.owners{background:rgba(34,197,94,.15);color:#4ade80;}
.rem-ex-detail{animation:F .2s;}
.rem-ex-detail-header{display:flex;gap:16px;margin-bottom:16px;}
.rem-ex-detail-img{width:100px;height:150px;border-radius:10px;overflow:hidden;flex-shrink:0;}
.rem-ex-detail-img img{width:100%;height:100%;object-fit:cover;}
.rem-ex-detail-meta{flex:1;display:flex;flex-direction:column;gap:6px;}
.rem-ex-detail-name{font-size:18px;font-weight:700;color:#fff;}
.rem-ex-detail-id{font-size:12px;color:#71717a;}
.rem-ex-user-list{margin-top:8px;}
.rem-ex-user-list h4{font-size:12px;font-weight:700;text-transform:uppercase;color:#71717a;margin:12px 0 6px;letter-spacing:.05em;}
.rem-ex-user{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;transition:.1s;text-decoration:none;}
.rem-ex-user:hover{background:#1e1e21;}
.rem-ex-user-name{font-size:13px;color:#e4e4e7;font-weight:500;}
.rem-ex-user-id{font-size:11px;color:#52525b;}
.rem-ex-empty{text-align:center;padding:40px 20px;color:#52525b;font-size:14px;}
.rem-ex-nav{display:flex;justify-content:center;gap:8px;margin-top:16px;}
.rem-scan-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;border:1px solid #27272a;background:#18181b;color:#a1a1aa;font-size:11px;font-weight:600;cursor:pointer;transition:.2s;margin-top:6px;align-self:center;max-width:130px;min-height:36px;flex-shrink:1;}
.rem-scan-btn:hover{border-color:#3f3f46;color:#fff;}
.rem-scan-btn.active{background:rgba(59,130,246,.15);border-color:#3b82f6;color:#60a5fa;}
.rem-scan-btn .rem-scan-label{white-space:normal;text-align:left;line-height:1.2;flex:1;}
.rem-scan-btn .rem-scan-check{width:16px;height:16px;border-radius:4px;border:2px solid #3f3f46;display:flex;align-items:center;justify-content:center;transition:.2s;flex-shrink:0;}
.rem-scan-btn.active .rem-scan-check{background:#3b82f6;border-color:#3b82f6;}
.rem-scan-btn.active .rem-scan-check svg{display:block;}
.rem-scan-btn .rem-scan-check svg{display:none;width:10px;height:10px;}
.rem-ex-scanlist{margin-bottom:16px;}
.rem-ex-scanlist-title{font-size:13px;font-weight:700;color:#a1a1aa;margin-bottom:8px;display:flex;align-items:center;gap:8px;}
.rem-ex-scanlist-count{font-size:11px;color:#52525b;font-weight:500;}
.rem-ex-scanlist-items{display:flex;flex-direction:column;gap:4px;max-height:200px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#27272a transparent;}
.rem-ex-scanlist-items::-webkit-scrollbar{width:5px;}
.rem-ex-scanlist-items::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px;}
.rem-ex-scanlist-item{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:#18181b;border:1px solid #1e1e21;border-radius:8px;transition:.1s;}
.rem-ex-scanlist-item:hover{border-color:#27272a;}
.rem-ex-scanlist-item-info{display:flex;align-items:center;gap:8px;min-width:0;}
.rem-ex-scanlist-item-name{font-size:13px;color:#e4e4e7;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.rem-ex-scanlist-item-id{font-size:11px;color:#52525b;flex-shrink:0;}
.rem-ex-scanlist-rm{width:24px;height:24px;border-radius:6px;border:none;background:transparent;color:#52525b;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.15s;flex-shrink:0;}
.rem-ex-scanlist-rm:hover{background:#27272a;color:#ef4444;}
.rem-ex-scanlist-actions{display:flex;gap:6px;margin-top:8px;}
#rem-custom-bg {
position:fixed;top:0;left:0;width:100%;height:100vh;
object-fit:cover;object-position:center center;
z-index:-9;pointer-events:none;
will-change:transform;transform:translateZ(0);
}
.rem-slider {
-webkit-appearance:none;appearance:none;
width:100%;height:4px;border-radius:2px;
background:#3f3f46;outline:none;cursor:pointer;
}
.rem-slider::-webkit-slider-thumb {
-webkit-appearance:none;appearance:none;
width:14px;height:14px;border-radius:50%;background:#22c55e;cursor:pointer;
}
.rem-slider::-moz-range-thumb {
width:14px;height:14px;border-radius:50%;background:#22c55e;cursor:pointer;border:none;
}
.rem-url-input {
width:100%;background:#0c0c0c;border:1px solid #3f3f46;border-radius:8px;
color:#e4e4e7;font-size:12px;padding:6px 10px;box-sizing:border-box;
outline:none;transition:border-color .15s;
}
.rem-url-input:focus { border-color:#22c55e; }
.rem-url-input::placeholder { color:#52525b; }
.rem-sub-row {
padding:4px 16px 8px;
display:flex;flex-direction:column;gap:6px;
}
.rem-sub-row-label {
font-size:11px;color:#71717a;
display:flex;justify-content:space-between;align-items:center;
}
.rem-upload-btn {
width:100%;height:30px;border-radius:8px;border:1px dashed #3f3f46;
background:transparent;color:#71717a;font-size:12px;font-weight:600;
cursor:pointer;transition:all .15s;
}
.rem-upload-btn:hover { border-color:#52525b;color:#a1a1aa;background:#27272a; }
.rem-select {
background:#0c0c0c;border:1px solid #3f3f46;border-radius:8px;
color:#e4e4e7;font-size:12px;padding:4px 8px;
outline:none;cursor:pointer;
}
.rem-deck-count-badge {
position:absolute!important;top:6px!important;left:6px!important;z-index:50!important;
min-width:24px;height:24px;border-radius:12px;
background:rgba(0,0,0,.75);color:#fff;
display:flex;align-items:center;justify-content:center;
padding:0 6px;font-size:12px;font-weight:700;
border:1.5px solid rgba(255,255,255,.25);pointer-events:none;
opacity:0;animation:remFadeIn .3s forwards;
backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);
letter-spacing:.3px;line-height:1;
}
.rem-deck-count-badge.has-count {
background:rgba(34,197,94,.85);border-color:rgba(255,255,255,.4);
}
.rem-deck-inner-count {
display:flex;align-items:center;justify-content:center;gap:6px;
margin-bottom:8px;font-size:14px;font-weight:600;color:#a1a1aa;
}
.rem-deck-inner-count strong {
color:#22c55e;font-size:16px;
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = STYLES;
(document.head || document.documentElement).appendChild(styleEl);
const Manager = {
covers: new Set(),
wishCovers: new Set(),
userId: null,
loaded: false,
wishLoaded: false,
cacheMap: new Map(),
onlineCache: new Map(),
onlineProcessing: new Set(),
async init() {
this.createUI();
this.userId = localStorage.getItem(MY_ID_KEY);
const cache = localStorage.getItem(MY_INV_KEY);
if (cache) {
try {
const d = JSON.parse(cache);
this.covers = new Set(d.data);
this.loaded = true;
setTimeout(() => scanVisual(), 500);
} catch (e) { }
}
const wishCache = localStorage.getItem(MY_WISH_KEY);
if (wishCache) {
try {
const wd = JSON.parse(wishCache);
this.wishCovers = new Set(wd.data);
this.wishLoaded = true;
} catch (e) { }
}
if (!this.userId) { this.promptId(); return; }
const cacheData = cache ? JSON.parse(cache) : null;
if (!cacheData || !cacheData.time || Date.now() - cacheData.time > 1200000) {
this.sync(true);
}
const wishCacheData = wishCache ? JSON.parse(wishCache) : null;
if (!wishCacheData || !wishCacheData.time || Date.now() - wishCacheData.time > 1200000) {
this.syncWishes(true);
}
setInterval(() => {
this.sync(true);
this.syncWishes(true);
}, 1200000);
},
getFilename(url) { if (!url) return null; return url.split('/').pop().split('?')[0].replace(/\.(jpg|jpeg|png|webp|gif|webm|mp4)$/i, ''); },
async sync(silent = false) {
if (!this.userId) return;
if (this.syncing) return;
this.syncing = true;
if (!silent) this.showStatus("Обновление...", true);
let page = 1, run = true;
const temp = new Set();
while (run) {
const url = `${API_DOMAIN}/api/v2/inventory/${this.userId}/?count=100&ordering=rank&page=${page}&type=cards`;
if (!silent) this.showStatus(`Загрузка стр. ${page} (Найдено: ${temp.size})...`, true);
const data = await this.req(url);
if (!data) break;
const list = data.results || [];
if (!list.length) break;
list.forEach(item => {
if (item.card && item.card.cover) {
if (item.card.cover.high) temp.add(this.getFilename(item.card.cover.high));
if (item.card.cover.mid) temp.add(this.getFilename(item.card.cover.mid));
}
});
if (!data.next) run = false;
else { page++; await new Promise(r => setTimeout(r, 100)); }
}
this.covers = temp;
this.loaded = true;
localStorage.setItem(MY_INV_KEY, JSON.stringify({ time: Date.now(), data: Array.from(temp) }));
if (!silent) {
this.showStatus(`Готово!`, true);
setTimeout(() => this.showStatus("", false), 2000);
}
scanVisual();
this.syncing = false;
},
async syncWishes(silent = true) {
if (!this.userId) return;
if (this.syncingWishes) return;
this.syncingWishes = true;
let page = 1, run = true;
const temp = new Set();
while (run) {
const url = `${SITE_DOMAIN}/api/v2/inventory/wishes/users/${this.userId}/?wish_type=1&page=${page}`;
const data = await this.req(url);
if (!data) break;
const list = data.results || [];
if (!list.length) break;
list.forEach(item => {
if (item.card && item.card.cover) {
if (item.card.cover.high) temp.add(this.getFilename(item.card.cover.high));
if (item.card.cover.mid) temp.add(this.getFilename(item.card.cover.mid));
}
});
if (!data.next) run = false;
else { page++; await new Promise(r => setTimeout(r, 100)); }
}
this.wishCovers = temp;
this.wishLoaded = true;
localStorage.setItem(MY_WISH_KEY, JSON.stringify({ time: Date.now(), data: Array.from(temp) }));
scanVisual();
this.syncingWishes = false;
},
req(url) {
const finalUrl = url.startsWith('http') ? url : API_DOMAIN + '/' + url.replace(/^\//, '');
return new Promise(resolve => {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
if (isIOS || typeof GM_xmlhttpRequest === 'undefined') {
fetch(finalUrl, { headers: { "Accept": "application/json" }, credentials: "omit" })
.then(r => r.ok ? r.json() : null)
.then(resolve)
.catch(() => fallbackGM(finalUrl, resolve));
} else {
fallbackGM(finalUrl, resolve);
}
function fallbackGM(u, res) {
if (typeof GM_xmlhttpRequest !== 'undefined') {
GM_xmlhttpRequest({
method: "GET", url: u, headers: { "Accept": "application/json" },
onload: r => { if (r.status == 200) try { res(JSON.parse(r.responseText)) } catch { res(null) } else res(null) },
onerror: () => res(null)
});
} else {
res(null);
}
}
});
},
async getUserInfo(uid) {
if (this.cacheMap.has(uid)) return this.cacheMap.get(uid);
const data = await this.req(`${API_DOMAIN}/api/v2/users/${uid}/`);
if (data) this.cacheMap.set(uid, data);
return data;
},
has(filename) { return this.loaded && filename && this.covers.has(filename); },
hasWish(filename) { return this.wishLoaded && filename && this.wishCovers.has(filename) && !this.has(filename); },
promptId() {
const link = prompt("Remanga Fix: Вставьте ссылку на профиль:", "");
if (link) {
const m = link.match(/user\/(\d+)/);
if (m && m[1]) { localStorage.setItem(MY_ID_KEY, m[1]); this.userId = m[1]; this.sync(); }
}
},
showStatus(text, show) {
const el = document.getElementById('rem-loader-status');
if (!el) return;
if (show) { el.innerText = text; el.classList.add('active'); } else el.classList.remove('active');
},
createUI() {
if (document.getElementById('rem-cfg-btn')) return;
applyCustomBackground();
applyTheme();
const btn = document.createElement('div');
btn.id = 'rem-cfg-btn';
btn.innerHTML = '⚙️';
btn.title = 'Настройки расширения';
btn.onclick = (e) => { e.stopPropagation(); togglePanel(); };
document.body.appendChild(btn);
const stat = document.createElement('div');
stat.id = 'rem-loader-status';
document.body.appendChild(stat);
document.body.appendChild(buildSettingsPanel());
document.addEventListener('click', (e) => {
const panel = document.getElementById('rem-settings-panel');
const cfgBtn = document.getElementById('rem-cfg-btn');
if (panel && !panel.contains(e.target) && e.target !== cfgBtn) {
panel.classList.remove('open');
}
});
}
};
function makeSwitchRow(key, name, desc) {
const row = document.createElement('div');
row.className = 'rem-row';
const info = document.createElement('div');
info.className = 'rem-row-info';
info.innerHTML = `<div class="rem-row-name">${name}</div>${desc ? `<div class="rem-row-desc">${desc}</div>` : ''}`;
const label = document.createElement('label');
label.className = 'rem-sw';
label.title = cfg[key] ? 'Включено' : 'Выключено';
const input = document.createElement('input');
input.type = 'checkbox';
input.checked = cfg[key];
const track = document.createElement('div');
track.className = 'rem-sw-track';
const thumb = document.createElement('div');
thumb.className = 'rem-sw-thumb';
label.appendChild(input);
label.appendChild(track);
label.appendChild(thumb);
input.addEventListener('change', () => {
saveSetting(key, input.checked);
label.title = input.checked ? 'Включено' : 'Выключено';
});
row.appendChild(info);
row.appendChild(label);
return row;
}
function buildSettingsPanel() {
const panel = document.createElement('div');
panel.id = 'rem-settings-panel';
panel.innerHTML = `
<div class="rem-panel-header">
<div class="rem-panel-title">⚙️ Настройки <span class="badge" style="font-size:10px;color:#71717a">v8.0.0</span></div>
</div>
`;
const body = document.createElement('div');
body.className = 'rem-panel-body';
const lbl1 = document.createElement('div');
lbl1.className = 'rem-section-label';
lbl1.textContent = 'Пользователи';
body.appendChild(lbl1);
body.appendChild(makeSwitchRow('onlineStatus', '🟢 Онлайн-статус', 'Точка на аватарке пользователя'));
body.appendChild(makeSwitchRow('profileStats', '🎴 Статистика профиля', 'Кол-во созданных карт на профиле'));
body.appendChild(makeSwitchRow('deckStats', '📦 Статистика кейсов', 'Число открытых паков на профиле'));
const lbl2 = document.createElement('div');
lbl2.className = 'rem-section-label';
lbl2.textContent = 'Карты';
body.appendChild(lbl2);
body.appendChild(makeSwitchRow('ownBadge', '✅ Метка коллекции', 'Галочка на картах из вашей коллекции'));
body.appendChild(makeSwitchRow('wishBadge', '💙 Метка желаемого', 'Синяя метка на картах из списка "Хочу"'));
body.appendChild(makeSwitchRow('cardStats', '📊 Статистика карты', 'Владельцы / Хотят / Продают в диалоге'));
const lblSystem = document.createElement('div');
lblSystem.className = 'rem-section-label';
lblSystem.textContent = 'Система';
body.appendChild(lblSystem);
const upBtn = document.createElement('button');
upBtn.className = 'rem-ex-btn primary';
upBtn.style.cssText = 'width:100%; margin-bottom:12px;';
upBtn.innerText = '🔄 Проверить обновление';
upBtn.onclick = () => checkForUpdates();
body.appendChild(upBtn);
const lbl2b = document.createElement('div');
lbl2b.className = 'rem-section-label';
lbl2b.textContent = 'Обмены';
body.appendChild(lbl2b);
body.appendChild(makeSwitchRow('scanButton', '➕ Кнопка сканирования', 'Чекбокс добавления в профилях'));
const exBtnRow = document.createElement('div');
exBtnRow.className = 'rem-row';
exBtnRow.style.cursor = 'pointer';
const exBtnInfo = document.createElement('div');
exBtnInfo.className = 'rem-row-info';
exBtnInfo.innerHTML = `<div class="rem-row-name">🔥 Сканер обменов</div><div class="rem-row-desc">Поиск обменов среди пользователей / гильдии</div>`;
exBtnRow.appendChild(exBtnInfo);
const exArrow = document.createElement('span');
exArrow.style.cssText = 'color:#52525b;font-size:16px;';
exArrow.textContent = '→';
exBtnRow.appendChild(exArrow);
exBtnRow.onclick = () => { document.getElementById('rem-settings-panel')?.classList.remove('open'); openExchangePanel(); };
body.appendChild(exBtnRow);
const lbl3 = document.createElement('div');
lbl3.className = 'rem-section-label';
lbl3.textContent = 'Фон сайта';
body.appendChild(lbl3);
const bgToggleRow = makeSwitchRow('customBgEnabled', '🖼️ Кастомный фон', 'Своё видео, гифка или фото');
body.appendChild(bgToggleRow);
const bgSub = document.createElement('div');
bgSub.className = 'rem-sub-row';
bgSub.id = 'rem-bg-sub';
bgSub.style.display = cfg.customBgEnabled ? 'flex' : 'none';
const urlLabel = document.createElement('div');
urlLabel.className = 'rem-sub-row-label';
urlLabel.textContent = 'URL (видео .webm/.mp4, гифка, фото)';
bgSub.appendChild(urlLabel);
const urlInput = document.createElement('input');
urlInput.type = 'text';
urlInput.className = 'rem-url-input';
urlInput.placeholder = 'https://... или вставьте ссылку';
urlInput.value = cfg.customBgUrl || '';
urlInput.addEventListener('change', () => {
saveSetting('customBgUrl', urlInput.value.trim());
applyCustomBackground();
});
bgSub.appendChild(urlInput);
const uploadBtn = document.createElement('button');
uploadBtn.className = 'rem-upload-btn';
uploadBtn.textContent = '📂 Загрузить файл с компьютера';
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*,video/*,.gif,.webm,.mp4,.webp';
fileInput.style.display = 'none';
fileInput.addEventListener('change', () => {
const file = fileInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const dataUrl = e.target.result;
saveSetting('customBgUrl', dataUrl);
urlInput.value = '(локальный файл: ' + file.name + ')';
applyCustomBackground();
};
reader.readAsDataURL(file);
});
uploadBtn.onclick = () => fileInput.click();
bgSub.appendChild(fileInput);
bgSub.appendChild(uploadBtn);
const opacLabel = document.createElement('div');
opacLabel.className = 'rem-sub-row-label';
const opacVal = document.createElement('span');
opacVal.textContent = cfg.customBgOpacity + '%';
opacLabel.innerHTML = 'Яркость фона: ';
opacLabel.appendChild(opacVal);
bgSub.appendChild(opacLabel);
const opacSlider = document.createElement('input');
opacSlider.type = 'range';
opacSlider.className = 'rem-slider';
opacSlider.min = 10; opacSlider.max = 100; opacSlider.step = 5;
opacSlider.value = cfg.customBgOpacity;
opacSlider.addEventListener('input', () => {
opacVal.textContent = opacSlider.value + '%';
saveSetting('customBgOpacity', Number(opacSlider.value));
applyCustomBackground();
});
bgSub.appendChild(opacSlider);
const blurLabel = document.createElement('div');
blurLabel.className = 'rem-sub-row-label';
const blurVal = document.createElement('span');
blurVal.textContent = cfg.customBgBlur + 'px';
blurLabel.innerHTML = 'Размытие: ';
blurLabel.appendChild(blurVal);
bgSub.appendChild(blurLabel);
const blurSlider = document.createElement('input');
blurSlider.type = 'range';
blurSlider.className = 'rem-slider';
blurSlider.min = 0; blurSlider.max = 20; blurSlider.step = 1;
blurSlider.value = cfg.customBgBlur;
blurSlider.addEventListener('input', () => {
blurVal.textContent = blurSlider.value + 'px';
saveSetting('customBgBlur', Number(blurSlider.value));
applyCustomBackground();
});
bgSub.appendChild(blurSlider);
const fitLabel = document.createElement('div');
fitLabel.className = 'rem-sub-row-label';
fitLabel.textContent = 'Масштаб:';
const fitSelect = document.createElement('select');
fitSelect.className = 'rem-select';
[['cover', 'Заполнить'], ['contain', 'Вписать'], ['fill', 'Растянуть']].forEach(([v, t]) => {
const opt = document.createElement('option');
opt.value = v; opt.textContent = t;
if (cfg.customBgFit === v) opt.selected = true;
fitSelect.appendChild(opt);
});
fitSelect.addEventListener('change', () => {
saveSetting('customBgFit', fitSelect.value);
applyCustomBackground();
});
fitLabel.appendChild(fitSelect);
bgSub.appendChild(fitLabel);
const resetBgBtn = document.createElement('button');
resetBgBtn.className = 'rem-upload-btn';
resetBgBtn.style.borderStyle = 'solid';
resetBgBtn.textContent = '🗑️ Убрать фон';
resetBgBtn.onclick = () => {
saveSetting('customBgUrl', '');
saveSetting('customBgEnabled', false);
urlInput.value = '';
const sw = bgToggleRow.querySelector('input[type=checkbox]');
if (sw) sw.checked = false;
document.getElementById('rem-bg-sub').style.display = 'none';
applyCustomBackground();
};
bgSub.appendChild(resetBgBtn);
body.appendChild(bgSub);
bgToggleRow.querySelector('input').addEventListener('change', function () {
document.getElementById('rem-bg-sub').style.display = this.checked ? 'flex' : 'none';
if (this.checked) applyCustomBackground();
else removeCustomBackground();
});
const lblTheme = document.createElement('div');
lblTheme.className = 'rem-section-label';
lblTheme.textContent = 'Оформление сайта';
body.appendChild(lblTheme);
const themeRow = document.createElement('div');
themeRow.style.cssText = 'padding:4px 16px 8px;display:flex;flex-direction:column;gap:8px;';
const themeLabel = document.createElement('div');
themeLabel.style.cssText = 'font-size:11px;color:#71717a;';
themeLabel.textContent = 'Цвет кнопок (Основа):';
themeRow.appendChild(themeLabel);
const presetGrid = document.createElement('div');
presetGrid.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;';
THEME_PRESETS.forEach(preset => {
const btn = document.createElement('button');
btn.title = preset.name;
btn.style.cssText = `
width:22px;height:22px;border-radius:50%;cursor:pointer;
border:2px solid ${cfg.themeButtonBg === preset.value ? '#fff' : 'transparent'};
background:${preset.value || '#3f3f46'};
transition:all .15s;outline:none;flex-shrink:0;
`;
if (!preset.value) {
btn.textContent = '✕';
btn.style.fontSize = '10px';
btn.style.color = '#a1a1aa';
}
btn.onclick = () => {
saveSetting('themeButtonBg', preset.value);
applyTheme();
presetGrid.querySelectorAll('button').forEach((b, i) => {
b.style.borderColor = THEME_PRESETS[i].value === preset.value ? '#fff' : 'transparent';
});
customBtnInput.value = preset.value || '#3b82f6';
};
presetGrid.appendChild(btn);
});
themeRow.appendChild(presetGrid);
const customBtnRow = document.createElement('div');
customBtnRow.style.cssText = 'display:flex;align-items:center;gap:8px;';
const customBtnLabel = document.createElement('span');
customBtnLabel.style.cssText = 'font-size:11px;color:#71717a;white-space:nowrap;width:100px;';
customBtnLabel.textContent = 'Свой цвет кнопок:';
const customBtnInput = document.createElement('input');
customBtnInput.type = 'color';
customBtnInput.value = cfg.themeButtonBg || '#3b82f6';
customBtnInput.style.cssText = 'width:32px;height:24px;border:1px solid #3f3f46;border-radius:6px;background:#0c0c0c;cursor:pointer;padding:2px;';
const customBtnReset = document.createElement('button');
customBtnReset.innerHTML = '✕';
customBtnReset.style.cssText = 'background:none;border:none;color:#a1a1aa;cursor:pointer;font-size:12px;outline:none;';
customBtnReset.title = 'Сбросить цвет кнопок';
customBtnReset.onclick = () => { saveSetting('themeButtonBg', ''); customBtnInput.value = '#3b82f6'; applyTheme(); presetGrid.querySelectorAll('button').forEach(b => b.style.borderColor = 'transparent'); };
customBtnInput.addEventListener('input', () => {
saveSetting('themeButtonBg', customBtnInput.value);
applyTheme();
presetGrid.querySelectorAll('button').forEach((b, i) => {
b.style.borderColor = THEME_PRESETS[i].value === customBtnInput.value ? '#fff' : 'transparent';
});
});
customBtnRow.appendChild(customBtnLabel);
customBtnRow.appendChild(customBtnInput);
customBtnRow.appendChild(customBtnReset);
themeRow.appendChild(customBtnRow);
const customAccentRow = document.createElement('div');
customAccentRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-top:4px;';
const customAccentLabel = document.createElement('span');
customAccentLabel.style.cssText = 'font-size:11px;color:#71717a;white-space:nowrap;width:100px;';
customAccentLabel.textContent = 'Цвет акцентов:';
const customAccentInput = document.createElement('input');
customAccentInput.type = 'color';
customAccentInput.value = cfg.themeAccent || '#3b82f6';
customAccentInput.style.cssText = 'width:32px;height:24px;border:1px solid #3f3f46;border-radius:6px;background:#0c0c0c;cursor:pointer;padding:2px;';
const customAccentReset = document.createElement('button');
customAccentReset.innerHTML = '✕';
customAccentReset.style.cssText = 'background:none;border:none;color:#a1a1aa;cursor:pointer;font-size:12px;outline:none;';
customAccentReset.title = 'Сбросить цвет акцентов';
customAccentReset.onclick = () => { saveSetting('themeAccent', ''); customAccentInput.value = '#3b82f6'; applyTheme(); };
customAccentInput.addEventListener('input', () => {
saveSetting('themeAccent', customAccentInput.value);
applyTheme();
});
customAccentRow.appendChild(customAccentLabel);
customAccentRow.appendChild(customAccentInput);
customAccentRow.appendChild(customAccentReset);
themeRow.appendChild(customAccentRow);
const nameColorRow = document.createElement('div');
nameColorRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-top:4px;';
const nameColorLabel = document.createElement('span');
nameColorLabel.style.cssText = 'font-size:11px;color:#71717a;white-space:nowrap;width:100px;';
nameColorLabel.textContent = 'Цвет имени:';
const nameColorInput = document.createElement('input');
nameColorInput.type = 'color';
nameColorInput.value = cfg.themeNameColor || '#ffffff';
nameColorInput.style.cssText = 'width:32px;height:24px;border:1px solid #3f3f46;border-radius:6px;background:#0c0c0c;cursor:pointer;padding:2px;';
const nameColorReset = document.createElement('button');
nameColorReset.innerHTML = '✕';
nameColorReset.style.cssText = 'background:none;border:none;color:#a1a1aa;cursor:pointer;font-size:12px;outline:none;';
nameColorReset.title = 'Сбросить цвет имени';
nameColorReset.onclick = () => { saveSetting('themeNameColor', ''); nameColorInput.value = '#ffffff'; applyTheme(); };
nameColorInput.addEventListener('input', () => { saveSetting('themeNameColor', nameColorInput.value); applyTheme(); });
nameColorRow.appendChild(nameColorLabel);
nameColorRow.appendChild(nameColorInput);
nameColorRow.appendChild(nameColorReset);
themeRow.appendChild(nameColorRow);
const nameFontRow = document.createElement('div');
nameFontRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-top:4px;';
const nameFontLabel = document.createElement('span');
nameFontLabel.style.cssText = 'font-size:11px;color:#71717a;white-space:nowrap;width:100px;';
nameFontLabel.textContent = 'Шрифт имени:';
const nameFontInput = document.createElement('select');
nameFontInput.style.cssText = 'flex:1;height:24px;border:1px solid #3f3f46;border-radius:6px;background:#0c0c0c;color:#fff;padding:0 6px;font-size:12px;outline:none;cursor:pointer;';
const fonts = [
{ v: '', t: 'По умолчанию' },
{ v: 'Comic Sans MS', t: 'Comic Sans' },
{ v: 'Caveat', t: 'Caveat (Рукописный)' },
{ v: 'Comfortaa', t: 'Comfortaa (Округлый)' },
{ v: 'Lobster', t: 'Lobster (Объемный)' },
{ v: 'Pacifico', t: 'Pacifico (Винтаж)' },
{ v: 'Oswald', t: 'Oswald (Строгий)' },
{ v: 'Press Start 2P', t: 'Пиксельный (8-bit)' },
{ v: 'Marmelad', t: 'Marmelad (Плавный)' },
{ v: 'Russo One', t: 'Russo One (Жирный/Квадратный)' },
{ v: 'Jura', t: 'Jura (Техно)' },
{ v: 'Marck Script', t: 'Marck Script (Каллиграфия)' },
{ v: 'Philosopher', t: 'Philosopher (Изящный)' },
{ v: 'Amatic SC', t: 'Amatic SC (Рисованный узкий)' },
{ v: 'Neucha', t: 'Neucha (Веселый)' },
{ v: 'Underdog', t: 'Underdog (Необычный)' }
];
fonts.forEach(f => {
const opt = document.createElement('option');
opt.value = opt.textContent = f.v;
opt.textContent = f.t;
if (cfg.themeNameFont === f.v) opt.selected = true;
nameFontInput.appendChild(opt);
});
const nameFontReset = document.createElement('button');
nameFontReset.innerHTML = '✕';
nameFontReset.style.cssText = 'background:none;border:none;color:#a1a1aa;cursor:pointer;font-size:12px;outline:none;';
nameFontReset.title = 'Сбросить шрифт имени';
nameFontReset.onclick = () => { saveSetting('themeNameFont', ''); nameFontInput.value = ''; applyTheme(); };
nameFontInput.addEventListener('change', () => { saveSetting('themeNameFont', nameFontInput.value); applyTheme(); });
nameFontRow.appendChild(nameFontLabel);
nameFontRow.appendChild(nameFontInput);
nameFontRow.appendChild(nameFontReset);
themeRow.appendChild(nameFontRow);
const menuBgRow = document.createElement('div');
menuBgRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-top:4px;';
const menuBgLabel = document.createElement('span');
menuBgLabel.style.cssText = 'font-size:11px;color:#71717a;white-space:nowrap;flex:1;';
menuBgLabel.textContent = 'Фон меню и карточек:';
const menuBgInput = document.createElement('input');
menuBgInput.type = 'color';
menuBgInput.value = cfg.themeMenuBg || '#18181b';
menuBgInput.style.cssText = 'width:32px;height:24px;border:1px solid #3f3f46;border-radius:6px;background:#0c0c0c;cursor:pointer;padding:2px;';
const menuBgReset = document.createElement('button');
menuBgReset.innerHTML = '✕';
menuBgReset.style.cssText = 'background:none;border:none;color:#a1a1aa;cursor:pointer;font-size:12px;outline:none;';
menuBgReset.title = 'Сбросить фон меню';
menuBgReset.onclick = () => { saveSetting('themeMenuBg', ''); menuBgInput.value = '#18181b'; applyTheme(); };
menuBgInput.addEventListener('input', () => { saveSetting('themeMenuBg', menuBgInput.value); applyTheme(); });
menuBgRow.appendChild(menuBgLabel);
menuBgRow.appendChild(menuBgInput);
menuBgRow.appendChild(menuBgReset);
themeRow.appendChild(menuBgRow);
const siteBgRow = document.createElement('div');
siteBgRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-top:4px;';
const siteBgLabel = document.createElement('span');
siteBgLabel.style.cssText = 'font-size:11px;color:#71717a;white-space:nowrap;flex:1;';
siteBgLabel.textContent = 'Фон сайта (без картинки):';
const siteBgInput = document.createElement('input');
siteBgInput.type = 'color';
siteBgInput.value = cfg.themeSiteBg || '#09090b';
siteBgInput.style.cssText = 'width:32px;height:24px;border:1px solid #3f3f46;border-radius:6px;background:#0c0c0c;cursor:pointer;padding:2px;';
const siteBgReset = document.createElement('button');
siteBgReset.innerHTML = '✕';
siteBgReset.style.cssText = 'background:none;border:none;color:#a1a1aa;cursor:pointer;font-size:12px;outline:none;';
siteBgReset.title = 'Сбросить фон сайта';
siteBgReset.onclick = () => { saveSetting('themeSiteBg', ''); siteBgInput.value = '#09090b'; applyTheme(); };
siteBgInput.addEventListener('input', () => { saveSetting('themeSiteBg', siteBgInput.value); applyTheme(); });
siteBgRow.appendChild(siteBgLabel);
siteBgRow.appendChild(siteBgInput);
siteBgRow.appendChild(siteBgReset);
themeRow.appendChild(siteBgRow);
body.appendChild(themeRow);
const lbl4 = document.createElement('div');
lbl4.className = 'rem-section-label';
lbl4.textContent = 'Исправления';
body.appendChild(lbl4);
body.appendChild(makeSwitchRow('fixMode', '🔧 FIX: переход на тайтл', 'Перейти на карты тайтла вместо простой ссылки'));
panel.appendChild(body);
const footer = document.createElement('div');
footer.className = 'rem-panel-footer';
const btnRefresh = document.createElement('button');
btnRefresh.className = 'rem-action-btn';
btnRefresh.textContent = '🔄 Обновить';
btnRefresh.title = 'Принудительно перезагрузить инвентарь';
btnRefresh.onclick = () => {
localStorage.removeItem(MY_INV_KEY);
localStorage.removeItem(MY_WISH_KEY);
Manager.loaded = false;
Manager.wishLoaded = false;
Manager.covers = new Set();
Manager.wishCovers = new Set();
Manager.sync();
Manager.syncWishes();
document.getElementById('rem-settings-panel')?.classList.remove('open');
};
const btnProfile = document.createElement('button');
btnProfile.className = 'rem-action-btn';
btnProfile.textContent = '👤 Профиль';
btnProfile.title = 'Ввести ссылку на другой профиль';
btnProfile.onclick = () => {
localStorage.removeItem(MY_INV_KEY);
localStorage.removeItem(MY_WISH_KEY);
localStorage.removeItem(MY_ID_KEY);
document.getElementById('rem-settings-panel')?.classList.remove('open');
Manager.userId = null;
Manager.loaded = false;
Manager.wishLoaded = false;
Manager.covers = new Set();
Manager.wishCovers = new Set();
Manager.promptId();
};
const btnReset = document.createElement('button');
btnReset.className = 'rem-action-btn danger';
btnReset.textContent = '🗑️ Сброс';
btnReset.title = 'Удалить все данные расширения и перезагрузить страницу';
btnReset.onclick = () => {
if (confirm('Сбросить все данные расширения?')) {
localStorage.removeItem(MY_INV_KEY);
localStorage.removeItem(MY_WISH_KEY);
localStorage.removeItem(MY_ID_KEY);
location.reload();
}
};
footer.appendChild(btnRefresh);
footer.appendChild(btnProfile);
footer.appendChild(btnReset);
panel.appendChild(footer);
return panel;
}
function togglePanel() {
const panel = document.getElementById('rem-settings-panel');
if (!panel) return;
panel.classList.toggle('open');
}
function applyCustomBackground() {
if (!cfg.customBgEnabled || !cfg.customBgUrl) { removeCustomBackground(); return; }
const url = cfg.customBgUrl;
const opacity = (cfg.customBgOpacity ?? 80) / 100;
const blur = cfg.customBgBlur ?? 0;
const fit = cfg.customBgFit || 'cover';
removeCustomBackground();
const isVideo = /\.(webm|mp4|ogg|ogv)(\?.*)?$/.test(url) || url.startsWith('data:video');
let bgStyle = document.getElementById('rem-bg-override');
if (!bgStyle) {
bgStyle = document.createElement('style');
bgStyle.id = 'rem-bg-override';
(document.head || document.documentElement).appendChild(bgStyle);
}
bgStyle.textContent = `
body, #__next,
[data-theme="light"] body, [data-theme="dark"] body,
[data-sentry-element="AppLayoutRoot"],
[data-sentry-component="AppLayoutRoot"],
[data-sentry-element="AppLayoutContent"],
[data-sentry-element="EntityLayoutRoot"] {
background: transparent !important;
background-color: transparent !important;
background-image: none !important;
}
.cs-layout-root::before {
display: none !important;
background: none !important;
}
[data-sentry-component="Header"], header {
background-color: var(--chakra-colors-gray-800, #18181b) !important;
backdrop-filter: none !important;
}
[data-theme="light"] [data-sentry-component="Header"],
[data-theme="light"] header {
background-color: var(--chakra-colors-white, #ffffff) !important;
}
[data-sentry-component="WallpaperBackground"],
.custom-background-video, video.custom-background-video {
z-index: -2 !important;
}
`;
let el;
if (isVideo) {
el = document.createElement('video');
el.src = url;
el.autoplay = true;
el.loop = true;
el.muted = true;
el.playsInline = true;
el.play().catch(() => { });
} else {
el = document.createElement('img');
el.src = url;
el.onerror = () => console.warn('REM BG: не удалось загрузить фон:', url.substring(0, 80));
}
el.id = 'rem-custom-bg';
el.style.cssText = [
'position:fixed',
'top:0', 'left:0',
'width:100%', 'height:100vh',
`object-fit:${fit}`,
'object-position:center center',
'z-index:-1',
'pointer-events:none',
'will-change:transform',
'transform:translateZ(0)',
`opacity:${opacity}`,
`filter:${blur > 0 ? 'blur(' + blur + 'px)' : 'none'}`,
'transition:opacity .3s',
].join(';');
document.body.insertBefore(el, document.body.firstChild);
}
function removeCustomBackground() {
const old = document.getElementById('rem-custom-bg');
if (old) old.remove();
const bgStyle = document.getElementById('rem-bg-override');
if (bgStyle) bgStyle.remove();
}
function hexToHsl(hex) {
let r = parseInt(hex.slice(1, 3), 16) / 255,
g = parseInt(hex.slice(3, 5), 16) / 255,
b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) { h = s = 0; } else {
const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; default: h = (r - g) / d + 4; }
h /= 6;
}
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
}
function applyTheme() {
let themeEl = document.getElementById('rem-theme-override');
if (!themeEl) {
themeEl = document.createElement('style');
themeEl.id = 'rem-theme-override';
(document.head || document.documentElement).appendChild(themeEl);
}
let css = '';
if (cfg.themeNameFont && cfg.themeNameFont !== '') {
if (cfg.themeNameFont !== 'Comic Sans MS') {
const fontNameUrl = cfg.themeNameFont.replace(/ /g, '+');
css += `@import url('https://fonts.googleapis.com/css2?family=${fontNameUrl}&display=swap');\n`;
}
}
if (cfg.themeAccent) {
const accent = cfg.themeAccent;
let darker = accent;
const darkerMatch = accent.match(/#([0-9a-f]{6})/i);
if (darkerMatch) {
const dr = Math.max(0, parseInt(darkerMatch[1].slice(0, 2), 16) - 30),
dg = Math.max(0, parseInt(darkerMatch[1].slice(2, 4), 16) - 30),
db = Math.max(0, parseInt(darkerMatch[1].slice(4, 6), 16) - 30);
darker = '#' + [dr, dg, db].map(v => v.toString(16).padStart(2, '0')).join('');
}
css += `
input:checked + .rem-sw-track { background: ${accent} !important; }
.rem-progress-fill { background: linear-gradient(90deg, ${accent}, ${darker}) !important; }
`;
}
if (cfg.themeButtonBg) {
const btnBg = cfg.themeButtonBg;
let darker = btnBg;
const darkerMatch = btnBg.match(/#([0-9a-f]{6})/i);
if (darkerMatch) {
const dr = Math.max(0, parseInt(darkerMatch[1].slice(0, 2), 16) - 30),
dg = Math.max(0, parseInt(darkerMatch[1].slice(2, 4), 16) - 30),
db = Math.max(0, parseInt(darkerMatch[1].slice(4, 6), 16) - 30);
darker = '#' + [dr, dg, db].map(v => v.toString(16).padStart(2, '0')).join('');
}
css += `
.bg-primary,
[data-state="active"][class*="bg-primary"],
[data-state="open"][class*="bg-primary"],
[aria-selected="true"][class*="bg-primary"] {
background-color: ${btnBg} !important;
border-color: ${btnBg} !important;
color: #fff !important;
}
.bg-primary:hover,
[data-state="active"][class*="bg-primary"]:hover,
[data-state="open"][class*="bg-primary"]:hover,
[aria-selected="true"][class*="bg-primary"]:hover {
background-color: ${darker} !important;
border-color: ${darker} !important;
}
`;
}
if (cfg.themeNameColor || cfg.themeNameFont) {
css += `
.cs-layout-title-text, .cs-layout-title .cs-text {
${cfg.themeNameColor ? `color: ${cfg.themeNameColor} !important;` : ''}
${cfg.themeNameFont ? `font-family: "${cfg.themeNameFont}", sans-serif !important;` : ''}
}
`;
}
if (cfg.themeMenuBg) {
const mBg = hexToHsl(cfg.themeMenuBg);
css += `
:root {
--popover: ${mBg} !important;
--card: ${mBg} !important;
--secondary: ${mBg} !important;
--muted: ${mBg} !important;
}
.bg-popover, .bg-secondary, .bg-card, .bg-muted, .cs-account-menu, [data-radix-menu-content] {
background-color: ${cfg.themeMenuBg} !important;
}
.rem-settings-panel, .rem-box, .rem-progress-box {
background-color: ${cfg.themeMenuBg} !important;
border-color: rgba(255,255,255,0.1) !important;
}
`;
}
if (cfg.themeSiteBg && !cfg.customBgEnabled) {
const sBg = hexToHsl(cfg.themeSiteBg);
css += `
:root {
--background: ${sBg} !important;
}
body, #__next, .bg-background {
background-color: ${cfg.themeSiteBg} !important;
}
`;
}
themeEl.textContent = css;
}
function removeTheme() {
const el = document.getElementById('rem-theme-override');
if (el) el.textContent = '';
}
const HOT_RANKS_EX = ['RE', 'S', 'A', 'B', 'C', 'D', 'E', 'F'];
function getCardRankLetter(card) {
let rk = card.rank || card.card_rank || '';
if (typeof rk === 'object') rk = rk.name || rk.rank || '';
rk = String(rk).trim().toUpperCase();
if (!rk) return '?';
const m = rk.match(/RANK_([A-Z]+)/i);
if (m) return m[1].toUpperCase();
for (const l of HOT_RANKS_EX) if (rk.includes(l)) return l;
return rk.slice(0, 2) || '?';
}
function getCardCharName(card) {
if (card.character && typeof card.character === 'object' && card.character.name) return card.character.name;
return card.name || '';
}
function isBetterCardInfo(newC, oldC) {
if (!oldC) return true;
const newN = getCardCharName(newC);
const oldN = getCardCharName(oldC);
const isGeneric = (n) => !n || n.startsWith('Карта #');
if (!isGeneric(newN) && isGeneric(oldN)) return true;
if (newC.character && !oldC.character) return true;
return false;
}
async function fetchUserTradeData(userId) {
const DOMAIN = SITE_DOMAIN;
const fetchPages = async (type) => {
let items = [], pg = 1, run = true;
while (run) {
const url = `${DOMAIN}/api/v2/inventory/wishes/users/${userId}/?wish_type=${type}&page=${pg}`;
const data = await Manager.req(url);
if (!data) break;
const list = data.results || [];
if (!list.length) break;
list.forEach(it => { if (it.card) items.push(it.card); });
if (!data.next) run = false; else { pg++; await new Promise(r => setTimeout(r, 80)); }
}
return items;
};
const fetchInv = async () => {
let items = [], pg = 1, run = true;
while (run) {
const url = `${API_DOMAIN}/api/v2/inventory/${userId}/?type=cards&count=50&page=${pg}`;
const data = await Manager.req(url);
if (!data) break;
const list = data.results || [];
if (!list.length) break;
list.forEach(it => { if (it.card) items.push(it.card); });
if (!data.next) run = false; else { pg++; await new Promise(r => setTimeout(r, 50)); }
}
return items;
};
let username = `User_${userId}`;
let sex = 0;
const udata = await Manager.req(`${API_DOMAIN}/api/v2/users/${userId}/`);
if (udata) {
username = udata.username || (udata.content && udata.content.username) || username;
const maybeSex = udata.sex !== undefined ? udata.sex : (udata.content && udata.content.sex !== undefined ? udata.content.sex : 0);
sex = parseInt(maybeSex) || 0;
}
const [wants, offers, inventory] = await Promise.all([fetchPages(1), fetchPages(2), fetchInv()]);
return { profile: { username, id: userId, sex }, wants, offers, inventory };
}
async function gatherTradesEx(userIds, progressCb) {
const usersData = {};
let done = 0;
const total = userIds.length;
const queue = [...userIds];
const workers = [];
for (let i = 0; i < Math.min(4, queue.length); i++) {
workers.push((async () => {
while (queue.length > 0) {
const uid = queue.shift();
try {
const data = await fetchUserTradeData(uid);
if (data) usersData[uid] = data;
} catch (e) { }
done++;
if (progressCb) progressCb(done, total);
}
})());
}
await Promise.all(workers);
const cardsDb = {};
for (const [uid, data] of Object.entries(usersData)) {
const prof = data.profile;
for (const c of data.wants) {
const cid = String(c.id);
if (!cid) continue;
if (!cardsDb[cid]) cardsDb[cid] = { card: c, wants: [], offers: [], inventory: [] };
else if (isBetterCardInfo(c, cardsDb[cid].card)) cardsDb[cid].card = c;
cardsDb[cid].wants.push(prof);
}
for (const c of data.offers) {
const cid = String(c.id);
if (!cid) continue;
if (!cardsDb[cid]) cardsDb[cid] = { card: c, wants: [], offers: [], inventory: [] };
else if (isBetterCardInfo(c, cardsDb[cid].card)) cardsDb[cid].card = c;
cardsDb[cid].offers.push(prof);
}
for (const c of (data.inventory || [])) {
const cid = String(c.id);
if (!cid) continue;
if (!cardsDb[cid]) cardsDb[cid] = { card: c, wants: [], offers: [], inventory: [] };
else if (isBetterCardInfo(c, cardsDb[cid].card)) cardsDb[cid].card = c;
cardsDb[cid].inventory.push(prof);
}
}
const validTrades = [];
for (const [cid, info] of Object.entries(cardsDb)) {
const wList = info.wants, oList = info.offers, invList = info.inventory;
const wFiltered = [], oFiltered = [], invFiltered = [];
for (const w of wList) if (!wFiltered.find(x => String(x.id) === String(w.id))) wFiltered.push(w);
for (const o of oList) if (!oFiltered.find(x => String(x.id) === String(o.id))) oFiltered.push(o);
const oIds = new Set(oFiltered.map(o => String(o.id)));
for (const inv of invList) {
if (!oIds.has(String(inv.id))) {
if (!invFiltered.find(x => String(x.id) === String(inv.id))) invFiltered.push(inv);
}
}
if (wFiltered.length > 0 || oFiltered.length > 0) {
validTrades.push({ card_id: cid, card: info.card, buyers: wFiltered, sellers: oFiltered, owners: invFiltered });
}
}
validTrades.sort((a, b) => parseInt(a.card_id) - parseInt(b.card_id));
return validTrades;
}
async function fetchClubMembers(slug) {
const users = [];
let pg = 1, run = true;
const escSlug = encodeURIComponent(slug);
while (run) {
const url = `${API_DOMAIN}/api/v2/clubs/${escSlug}/members/?count=100&page=${pg}`;
const data = await Manager.req(url);
if (!data) break;
const list = data.results || [];
if (!list.length) break;
list.forEach(it => { if (it.user && it.user.id) users.push(it.user.id); });
if (!data.next) run = false; else { pg++; await new Promise(r => setTimeout(r, 80)); }
}
return users;
}
function openExchangePanel() {
let panel = document.getElementById('rem-exchange-panel');
let backdrop = document.querySelector('.rem-ex-backdrop');
if (!panel) {
backdrop = document.createElement('div');
backdrop.className = 'rem-ex-backdrop';
backdrop.onclick = () => closeExchangePanel();
document.body.appendChild(backdrop);
panel = document.createElement('div');
panel.id = 'rem-exchange-panel';
panel.innerHTML = `
<div class="rem-ex-header">
<div class="rem-ex-title">🔥 Сканер обменов</div>
<div class="rem-ex-close" id="rem-ex-close-btn">✕</div>
</div>
<div class="rem-ex-body" id="rem-ex-body"></div>
`;
document.body.appendChild(panel);
panel.querySelector('#rem-ex-close-btn').onclick = () => closeExchangePanel();
}
backdrop.classList.add('open');
panel.classList.add('open');
renderExchangeMain();
}
function closeExchangePanel() {
document.getElementById('rem-exchange-panel')?.classList.remove('open');
document.querySelector('.rem-ex-backdrop')?.classList.remove('open');
}
function fmtAgo(ts) {
const ago = Math.round((Date.now() - ts) / 60000);
if (ago < 1) return 'только что';
if (ago < 60) return `${ago} мин. назад`;
if (ago < 1440) return `${Math.round(ago / 60)} ч. назад`;
return `${Math.round(ago / 1440)} дн. назад`;
}
function renderExchangeMain() {
const body = document.getElementById('rem-ex-body');
if (!body) return;
const hasResults = ExState.results.length > 0;
const scanList = getScanList();
const history = getHistory();
let scanListHtml = '';
if (scanList.length > 0) {
scanListHtml = `
<div class="rem-ex-scanlist">
<div class="rem-ex-scanlist-title">👥 Список на сканирование <span class="rem-ex-scanlist-count">(${scanList.length} чел.)</span></div>
<div class="rem-ex-scanlist-items" id="rem-ex-scanlist-items">
${scanList.map(u => `
<div class="rem-ex-scanlist-item" data-uid="${u.id}">
<div class="rem-ex-scanlist-item-info">
<span class="rem-ex-scanlist-item-name">${u.username}</span>
<span class="rem-ex-scanlist-item-id">ID: ${u.id}</span>
</div>
<button class="rem-ex-scanlist-rm" data-rm-uid="${u.id}" title="Убрать">✕</button>
</div>
`).join('')}
</div>
<div class="rem-ex-scanlist-actions">
<button class="rem-ex-btn primary" id="rem-ex-scan-list-btn" style="height:36px;">🔍 Сканировать список (${scanList.length})</button>
<button class="rem-ex-btn secondary" id="rem-ex-clear-list-btn" style="height:36px;">🗑️ Очистить</button>
</div>
</div>
`;
}
let histHtml = '';
if (history.length > 0) {
histHtml = `
<div class="rem-ex-scanlist" style="margin-bottom:${hasResults ? '0' : '16'}px;">
<div class="rem-ex-scanlist-title">📋 История сканирований <span class="rem-ex-scanlist-count">(${history.length})</span></div>
<div class="rem-ex-scanlist-items">
${history.map(h => `
<div class="rem-ex-scanlist-item" data-hist-id="${h.id}" style="cursor:pointer;">
<div class="rem-ex-scanlist-item-info">
<span class="rem-ex-scanlist-item-name" style="color:${ExState.activeHistId === h.id ? '#60a5fa' : '#e4e4e7'};">${h.name}</span>
<span class="rem-ex-scanlist-item-id">${h.count} обменов · ${fmtAgo(h.time)}</span>
</div>
<button class="rem-ex-scanlist-rm" data-del-hist="${h.id}" title="Удалить">✕</button>
</div>
`).join('')}
</div>
</div>
`;
}
let cacheBarHtml = '';
if (hasResults && ExState.lastSource) {
const h = history.find(x => x.id === ExState.activeHistId);
const canUpdate = h && h.link && !ExState.scanning;
cacheBarHtml = `<div style="display:flex;flex-direction:column;gap:12px;margin-bottom:12px;padding:12px;background:#18181b;border:1px solid #27272a;border-radius:10px;">
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="font-size:12px;color:#71717a;">📦 <strong style="color:#a1a1aa;">${ExState.lastSource}</strong> · Всего: ${ExState.results.length}</div>
<div style="display:flex;gap:8px;">
${canUpdate ? `<button class="rem-ex-btn secondary" id="rem-ex-update-btn" style="height:28px;padding:0 12px;font-size:11px;">🔄 Обновить</button>` : ''}
<button class="rem-ex-btn secondary" id="rem-ex-close-results" style="height:28px;padding:0 12px;font-size:11px;">✕ Закрыть</button>
</div>
</div>
<div style="display:flex;gap:8px;">
<input type="text" id="rem-search-card" class="rem-ex-input" style="height:32px;font-size:12px;flex:1;" placeholder="🔍 Поиск карты (Название, ID или ссылка...)" value="${(ExState.searchQuery || '').replace(/"/g, '"')}">
<input type="text" id="rem-search-user" class="rem-ex-input" style="height:32px;font-size:12px;flex:1;" placeholder="👤 Поиск человека (Имя или ID...)" value="${(ExState.searchUser || '').replace(/"/g, '"')}">
</div>
</div>`;
}
let progressHtml = '';
if (ExState.scanning) {
const pct = ExState.scanTotal ? Math.round((ExState.scanDone / ExState.scanTotal) * 100) : 0;
progressHtml = `<div class="rem-ex-progress"><div class="rem-ex-ptext">Сканирование <strong>${ExState.scanDone}</strong> / <strong>${ExState.scanTotal}</strong> пользователей... (${pct}%)</div><div class="rem-ex-pbar-track"><div class="rem-ex-pbar-fill" id="rem-ex-pbar" style="width:${pct}%"></div></div></div>`;
}
body.innerHTML = `
<div class="rem-ex-input-row">
<input class="rem-ex-input" id="rem-ex-input" placeholder="Ссылка на гильдию или ID пользователей" />
<button class="rem-ex-btn primary" id="rem-ex-scan-btn" style="height:40px;" ${ExState.scanning ? 'disabled' : ''}>${ExState.scanning ? '⏳ Загрузка...' : '🔍 Скан'}</button>
</div>
${scanListHtml}
${!hasResults ? histHtml : ''}
<div id="rem-ex-status">${progressHtml}</div>
${hasResults ? cacheBarHtml + '<div id="rem-ex-results-container"></div>' : (history.length === 0 && scanList.length === 0 && !ExState.scanning ? `
<div class="rem-ex-empty">
<div style="font-size:32px;margin-bottom:12px;">🔥</div>
<div>Введите ссылку на гильдию<br/>или добавляйте пользователей через профили</div>
<div style="margin-top:8px;font-size:12px;color:#3f3f46;">Пример: ${SITE_DOMAIN}/guild/fairy-tail-68c526d1</div>
<div style="margin-top:4px;font-size:12px;color:#3f3f46;">или: 12345 67890 11111</div>
</div>
` : '')}
`;
if (hasResults) renderExchangeResults();
document.getElementById('rem-ex-scan-btn').onclick = () => startExchangeScan();
document.getElementById('rem-ex-input').addEventListener('keypress', e => { if (e.key === 'Enter') startExchangeScan(); });
document.querySelectorAll('[data-rm-uid]').forEach(btn => {
btn.onclick = (e) => { e.stopPropagation(); removeFromScanList(btn.getAttribute('data-rm-uid')); renderExchangeMain(); };
});
const scanListBtn = document.getElementById('rem-ex-scan-list-btn');
if (scanListBtn) { scanListBtn.onclick = () => startExchangeScanFromList(); }
const clearListBtn = document.getElementById('rem-ex-clear-list-btn');
if (clearListBtn) { clearListBtn.onclick = () => { saveScanList([]); renderExchangeMain(); }; }
document.querySelectorAll('[data-hist-id]').forEach(row => {
const handleHistClick = () => {
const hid = row.getAttribute('data-hist-id');
const h = history.find(x => x.id === hid);
const statusEl = document.getElementById('rem-ex-status');
if (statusEl) statusEl.innerHTML = `<div class="rem-ex-ptext">📂 Загрузка истории <strong>${h ? h.name : ''}</strong>...</div>`;
setTimeout(() => {
const data = loadFromHistory(hid);
if (!data || !data.length) {
if (statusEl) statusEl.innerHTML = `<div class="rem-ex-ptext" style="color:#ef4444;">❌ Не удалось загрузить данные (возможно, они были удалены или слишком велики)</div>`;
return;
}
ExState.results = data;
ExState.lastSource = h ? h.name : 'Из истории';
ExState.activeHistId = hid;
ExState.currentRank = 'ALL';
ExState.page = 0;
renderExchangeMain();
}, 50);
};
row.onclick = handleHistClick;
row.onpointerdown = (e) => {
if (e.pointerType === 'touch') row._remTouch = true;
};
row.onpointerup = (e) => {
if (e.pointerType === 'touch' && row._remTouch) {
row._remTouch = false;
handleHistClick();
}
};
});
document.querySelectorAll('[data-del-hist]').forEach(btn => {
btn.onclick = (e) => { e.stopPropagation(); deleteFromHistory(btn.getAttribute('data-del-hist')); renderExchangeMain(); };
});
const closeBtn = document.getElementById('rem-ex-close-results');
if (closeBtn) { closeBtn.onclick = () => { ExState.results = []; ExState.lastSource = ''; ExState.activeHistId = null; renderExchangeMain(); }; }
const updateBtn = document.getElementById('rem-ex-update-btn');
if (updateBtn) {
updateBtn.onclick = () => {
const h = history.find(x => x.id === ExState.activeHistId);
if (h && h.link) {
const input = document.getElementById('rem-ex-input');
if (input) input.value = h.link;
startExchangeScan();
}
};
}
const searchCard = document.getElementById('rem-search-card');
const searchUser = document.getElementById('rem-search-user');
if (searchCard) searchCard.addEventListener('input', (e) => { ExState.searchQuery = e.target.value.trim(); ExState.page = 0; renderExchangeResults(); });
if (searchUser) searchUser.addEventListener('input', (e) => { ExState.searchUser = e.target.value.trim(); ExState.page = 0; renderExchangeResults(); });
}
async function startExchangeScanFromList() {
const scanList = getScanList();
if (!scanList.length) return;
const statusEl = document.getElementById('rem-ex-status');
if (!statusEl) return;
if (ExState.scanning) return;
ExState.scanning = true;
const scanBtn = document.getElementById('rem-ex-scan-list-btn');
if (scanBtn) { scanBtn.disabled = true; scanBtn.textContent = '⏳ Загрузка...'; }
const userIds = scanList.map(u => parseInt(u.id));
ExState.scanDone = 0;
ExState.scanTotal = userIds.length;
renderExchangeMain();
const results = await gatherTradesEx(userIds, (done, total, msg) => {
ExState.scanDone = done;
ExState.scanTotal = total;
const pct = Math.round((done / total) * 100);
const pbar = document.getElementById('rem-ex-pbar');
const ptext = document.querySelector('.rem-ex-ptext');
if (pbar) pbar.style.width = pct + '%';
if (ptext) ptext.innerHTML = msg || `Сканирование <strong>${done}</strong> / <strong>${total}</strong> пользователей... (${pct}%)`;
});
const srcName = `Список (${scanList.length} чел.)`;
ExState.results = results;
ExState.lastSource = srcName;
ExState.currentRank = 'ALL';
ExState.page = 0;
ExState.scanning = false;
ExState.activeHistId = saveToHistory(srcName, results, "list");
renderExchangeMain();
}
async function fetchGuildName(slug) {
try {
const data = await Manager.req(`${API_DOMAIN}/api/v2/clubs/${encodeURIComponent(slug)}/`);
if (data) {
const name = data.name || (data.content && data.content.name);
if (name) return name;
}
} catch (e) { }
return slug;
}
async function startExchangeScan() {
const input = document.getElementById('rem-ex-input');
const statusEl = document.getElementById('rem-ex-status');
if (!input || !statusEl) return;
const val = input.value.trim();
if (!val) return;
if (ExState.scanning) return;
ExState.scanning = true;
const scanBtn = document.getElementById('rem-ex-scan-btn');
if (scanBtn) { scanBtn.disabled = true; scanBtn.textContent = '⏳ Загрузка...'; }
let userIds = [];
let srcName = '';
const clubMatch = val.match(/(?:clubs|guild)\/([^\/\?\s]+)/);
if (clubMatch) {
statusEl.innerHTML = `<div class="rem-ex-progress"><div class="rem-ex-ptext">Загрузка гильдии <strong>${clubMatch[1]}</strong>...</div><div class="rem-ex-pbar-track"><div class="rem-ex-pbar-fill" id="rem-ex-pbar" style="width:5%"></div></div></div>`;
const [members, guildName] = await Promise.all([fetchClubMembers(clubMatch[1]), fetchGuildName(clubMatch[1])]);
userIds = members;
srcName = guildName;
if (!userIds.length) {
statusEl.innerHTML = `<div class="rem-ex-ptext" style="color:#ef4444;">❌ Не удалось загрузить участников гильдии</div>`;
ExState.scanning = false;
if (scanBtn) { scanBtn.disabled = false; scanBtn.textContent = '🔍 Скан'; }
return;
}
} else {
const links = val.match(/(?:remanga\.org|xn--80aaig9ahr\.xn--c1avg)\/user\/(\d+)/g) || [];
links.forEach(l => { const m = l.match(/(\d+)/); if (m) userIds.push(parseInt(m[1])); });
const rawText = val.replace(/https?:\/\/\S+/g, '');
const nums = rawText.match(/\b(\d+)\b/g) || [];
nums.forEach(n => userIds.push(parseInt(n)));
userIds = [...new Set(userIds)];
srcName = `${userIds.length} пользователей`;
if (!userIds.length) {
statusEl.innerHTML = `<div class="rem-ex-ptext" style="color:#ef4444;">❌ Не найдено ID пользователей</div>`;
ExState.scanning = false;
if (scanBtn) { scanBtn.disabled = false; scanBtn.textContent = '🔍 Скан'; }
return;
}
}
ExState.scanDone = 0;
ExState.scanTotal = userIds.length;
renderExchangeMain();
const results = await gatherTradesEx(userIds, (done, total, msg) => {
ExState.scanDone = done;
ExState.scanTotal = total;
const pct = Math.round((done / total) * 100);
const pbar = document.getElementById('rem-ex-pbar');
const ptext = document.querySelector('.rem-ex-ptext');
if (pbar) pbar.style.width = pct + '%';
if (ptext) ptext.innerHTML = msg || `Сканирование <strong>${done}</strong> / <strong>${total}</strong> пользователей... (${pct}%)`;
});
ExState.results = results;
ExState.lastSource = srcName;
ExState.currentRank = 'ALL';
ExState.page = 0;
ExState.scanning = false;
ExState.activeHistId = saveToHistory(srcName, results, val);
renderExchangeMain();
}
function filterByRankEx(results, rank) {
if (rank === 'ALL') return results;
return results.filter(r => getCardRankLetter(r.card) === rank);
}
function getFilteredExchangeResults() {
let results = ExState.results;
if (ExState.searchQuery) {
const sq = ExState.searchQuery.toLowerCase();
results = results.filter(r => {
const cardName = (getCardCharName(r.card) || '').toLowerCase();
const cardId = String(r.card_id);
let queryId = sq;
if (sq.includes('/card/')) queryId = sq.match(/\/card\/(\d+)/)?.[1] || sq;
return cardName.includes(sq) || cardId === queryId || cardId.includes(queryId);
});
}
if (ExState.searchUser) {
const su = ExState.searchUser.toLowerCase();
results = results.filter(r => {
const matchUser = (u) => String(u.id) === su || String(u.id).includes(su) || (u.username && u.username.toLowerCase().includes(su));
return r.sellers.some(matchUser) || r.buyers.some(matchUser) || r.owners.some(matchUser);
});
}
return results;
}
function renderExchangeResults() {
const container = document.getElementById('rem-ex-results-container');
if (!container) return;
const results = getFilteredExchangeResults();
if (!results.length && ExState.results.length) {
container.innerHTML = `<div class="rem-ex-empty"><div style="font-size:32px;margin-bottom:12px;">🕵️♂️</div><div>По вашему запросу ничего не найдено</div></div>`;
return;
} else if (!results.length) {
container.innerHTML = `<div class="rem-ex-empty"><div style="font-size:32px;margin-bottom:12px;">🤷</div><div>Обмены не найдены</div></div>`;
return;
}
const rankCounts = {};
results.forEach(r => {
const rk = getCardRankLetter(r.card);
rankCounts[rk] = (rankCounts[rk] || 0) + 1;
});
let html = `<div class="rem-ex-summary">
<div class="rem-ex-s-item"><div class="rem-ex-s-num" style="color:#60a5fa;">${results.length}</div><div class="rem-ex-s-lab">Обменов</div></div>
<div class="rem-ex-s-item"><div class="rem-ex-s-num" style="color:#fb923c;">${results.reduce((s, r) => s + r.sellers.length, 0)}</div><div class="rem-ex-s-lab">Продают</div></div>
<div class="rem-ex-s-item"><div class="rem-ex-s-num" style="color:#3b82f6;">${results.reduce((s, r) => s + r.buyers.length, 0)}</div><div class="rem-ex-s-lab">Хотят</div></div>
<div class="rem-ex-s-item"><div class="rem-ex-s-num" style="color:#4ade80;">${results.reduce((s, r) => s + r.owners.length, 0)}</div><div class="rem-ex-s-lab">Есть</div></div>
</div>`;
html += `<div class="rem-ex-rank-bar">`;
html += `<div class="rem-ex-rank-btn ${ExState.currentRank === 'ALL' ? 'active' : ''}" data-rank="ALL">💎 Все <span class="rem-ex-rank-count">(${results.length})</span></div>`;
HOT_RANKS_EX.forEach(rk => {
const cnt = rankCounts[rk] || 0;
if (cnt > 0) {
html += `<div class="rem-ex-rank-btn ${ExState.currentRank === rk ? 'active' : ''}" data-rank="${rk}">${rk} <span class="rem-ex-rank-count">(${cnt})</span></div>`;
}
});
html += `</div>`;
const pool = filterByRankEx(results, ExState.currentRank);
const totalPages = Math.max(1, Math.ceil(pool.length / ExState.perPage));
if (ExState.page >= totalPages) ExState.page = 0;
const start = ExState.page * ExState.perPage;
const batch = pool.slice(start, start + ExState.perPage);
html += `<div class="rem-ex-results">`;
batch.forEach((item, i) => {
const card = item.card;
const name = getCardCharName(card) || `Карта #${item.card_id}`;
const rank = getCardRankLetter(card);
const RANK_COLORS = { RE: '#f59e0b', S: '#fbbf24', A: '#c084fc', B: '#60a5fa', C: '#4ade80', D: '#fff', E: '#9ca3af', F: '#9ca3af' };
const color = RANK_COLORS[rank] || '#a1a1aa';
const cov = card.cover || {};
const imgUrl = typeof cov === 'object' ? (cov.high || cov.mid || '') : '';
const fullImg = imgUrl.startsWith('http') ? imgUrl : (imgUrl ? `${SITE_DOMAIN}${imgUrl}` : '');
const isVid = isVideoUrl(fullImg);
html += `
<div class="rem-ex-card" data-trade-idx="${start + i}">
<div class="rem-ex-card-inner">
<div class="rem-ex-card-img"${isVid ? ` data-video-src="${fullImg}"` : ''}>${fullImg && !isVid ? `<img src="${fullImg}" loading="lazy">` : (!fullImg ? '' : '')}</div>
<div class="rem-ex-card-info">
<div class="rem-ex-card-name">${name}</div>
<div class="rem-ex-card-rank" style="color:${color};border:1px solid ${color};background:rgba(0,0,0,.3);">${rank}</div>
<div class="rem-ex-card-traders">
${item.sellers.length ? `<span class="rem-ex-card-tag sellers">🔶 Отдают: ${item.sellers.length}</span>` : ''}
${item.buyers.length ? `<span class="rem-ex-card-tag buyers">🔷 Хотят: ${item.buyers.length}</span>` : ''}
${item.owners.length ? `<span class="rem-ex-card-tag owners">🟢 Есть: ${item.owners.length}</span>` : ''}
</div>
</div>
</div>
</div>
`;
});
html += `</div>`;
if (totalPages > 1) {
html += `<div class="rem-ex-nav">`;
if (ExState.page > 0) html += `<button class="rem-ex-btn secondary" data-page="${ExState.page - 1}">⬅️</button>`;
html += `<span style="color:#71717a;font-size:12px;display:flex;align-items:center;">Стр. ${ExState.page + 1} / ${totalPages}</span>`;
if (ExState.page < totalPages - 1) html += `<button class="rem-ex-btn secondary" data-page="${ExState.page + 1}">➡️</button>`;
html += `</div>`;
}
container.innerHTML = html;
container.querySelectorAll('[data-video-src]').forEach(el => {
renderCardMedia(el.getAttribute('data-video-src'), el);
});
container.querySelectorAll('.rem-ex-rank-btn').forEach(btn => {
btn.onclick = () => {
ExState.currentRank = btn.getAttribute('data-rank');
ExState.page = 0;
renderExchangeResults();
};
});
container.querySelectorAll('.rem-ex-card').forEach(card => {
card.onclick = () => {
const idx = parseInt(card.getAttribute('data-trade-idx'));
renderExchangeDetail(idx);
};
});
container.querySelectorAll('[data-page]').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
ExState.page = parseInt(btn.getAttribute('data-page'));
renderExchangeResults();
};
});
}
function renderExchangeDetail(idx) {
const container = document.getElementById('rem-ex-results-container');
if (!container) return;
const results = getFilteredExchangeResults();
const pool = filterByRankEx(results, ExState.currentRank);
if (idx < 0 || idx >= pool.length) return;
const data = pool[idx];
const card = data.card;
const name = getCardCharName(card) || `Карта #${data.card_id}`;
const rank = getCardRankLetter(card);
const cov = card.cover || {};
const imgUrl = typeof cov === 'object' ? (cov.high || cov.mid || '') : '';
const fullImg = imgUrl.startsWith('http') ? imgUrl : (imgUrl ? `${SITE_DOMAIN}${imgUrl}` : '');
const isVid = isVideoUrl(fullImg);
const RANK_COLORS = { RE: '#f59e0b', S: '#fbbf24', A: '#c084fc', B: '#60a5fa', C: '#4ade80', D: '#fff', E: '#9ca3af', F: '#9ca3af' };
const color = RANK_COLORS[rank] || '#a1a1aa';
const formatUsers = (users) => {
if (!users.length) return '<div style="color:#52525b;font-size:12px;padding:4px 10px;">—</div>';
return users.map(u => {
const emoji = u.sex === 1 ? '👨' : (u.sex === 2 ? '👩' : '👤');
return `
<a href="${SITE_DOMAIN}/user/${u.id}/about" target="_blank" class="rem-ex-user">
<span>${emoji}</span>
<span class="rem-ex-user-name">${u.username}</span>
<span class="rem-ex-user-id">ID: ${u.id}</span>
</a>
`}).join('');
};
container.innerHTML = `
<div class="rem-ex-detail">
<div style="margin-bottom:16px;">
<button class="rem-ex-btn secondary" id="rem-ex-back-btn">⬅️ Назад к списку</button>
</div>
<div class="rem-ex-detail-header">
<div class="rem-ex-detail-img"${isVid ? ` data-video-src="${fullImg}"` : ''}>${fullImg && !isVid ? `<img src="${fullImg}">` : (!fullImg ? '<div style="width:100%;height:100%;background:#27272a;display:flex;align-items:center;justify-content:center;color:#52525b;">?</div>' : '')}</div>
<div class="rem-ex-detail-meta">
<div class="rem-ex-detail-name">${name}</div>
<div class="rem-ex-card-rank" style="color:${color};border:1px solid ${color};background:rgba(0,0,0,.3);">${rank}</div>
<div class="rem-ex-detail-id">ID: ${data.card_id}</div>
<a href="${SITE_DOMAIN}/card/${data.card_id}" target="_blank" style="color:#3b82f6;font-size:12px;text-decoration:none;">Открыть на сайте ↗</a>
</div>
</div>
<div class="rem-ex-user-list">
<h4>🔶 Отдают (${data.sellers.length})</h4>
${formatUsers(data.sellers)}
<h4>🔷 Хотят (${data.buyers.length})</h4>
${formatUsers(data.buyers)}
${data.owners.length ? `<h4>🟢 Просто есть (${data.owners.length})</h4>${formatUsers(data.owners)}` : ''}
</div>
</div>
`;
container.querySelector('#rem-ex-back-btn').onclick = () => renderExchangeResults();
container.querySelectorAll('[data-video-src]').forEach(el => { renderCardMedia(el.getAttribute('data-video-src'), el); });
}
function createCheck() {
const div = document.createElement('div');
div.className = 'rem-own-badge';
div.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
return div;
}
function createWishCheck() {
const div = document.createElement('div');
div.className = 'rem-wish-badge';
div.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>`;
return div;
}
function processCardElement(mediaEl, src) {
if (mediaEl.hasAttribute('data-rem-checked')) return;
mediaEl.setAttribute('data-rem-checked', '1');
const fname = Manager.getFilename(src);
const have = Manager.has(fname);
const wish = Manager.hasWish(fname);
// If the element is a source, we want to attach the badge to the picture or video wrapper
let wrapper = mediaEl.parentElement;
if (mediaEl.tagName.toLowerCase() === 'source' && wrapper) {
wrapper = wrapper.parentElement;
}
if (!wrapper) return;
if (window.getComputedStyle(wrapper).position === 'static') wrapper.style.position = 'relative';
const isTitleCardsPage = !!location.pathname.match(/^\/manga\/([\w-]+)\/cards/);
const isDeletedTitle = new URLSearchParams(location.search).has('fix_id');
const isUserProfilePage = !!location.pathname.match(/\/user\/(?:\d+\/)?(about|inventory|create\/exchange|inventory\/cards-upgrade)/);
const hideOwnBadge = (isTitleCardsPage && !isDeletedTitle) || isUserProfilePage;
if (cfg.ownBadge && have && !hideOwnBadge && !wrapper.querySelector('.rem-own-badge')) {
wrapper.appendChild(createCheck());
}
if (cfg.wishBadge && wish && !have && !wrapper.querySelector('.rem-wish-badge')) {
wrapper.appendChild(createWishCheck());
}
}
function scanVisual() {
if (!Manager.loaded) return;
document.querySelectorAll('img[src*="/media/card-item/"]:not([data-rem-checked])').forEach(img => {
processCardElement(img, img.src);
});
document.querySelectorAll('video[src*="/media/card-item/"]:not([data-rem-checked])').forEach(vid => {
processCardElement(vid, vid.src);
});
document.querySelectorAll('video:not([data-rem-checked]), picture:not([data-rem-checked])').forEach(media => {
const source = media.querySelector('source[src*="/media/card-item/"], source[srcset*="/media/card-item/"]');
if (source) {
const src = source.srcset ? source.srcset.split(' ')[0] : source.src;
processCardElement(source, src);
}
});
document.querySelectorAll('[style*="/media/card-item/"]:not([data-rem-checked])').forEach(el => {
const style = el.getAttribute('style');
const match = style.match(/\/media\/card-item\/[^)"']+/);
if (match) {
processCardElement(el, match[0]);
}
});
const isTitleCardsPage = !!location.pathname.match(/^\/manga\/([\w-]+)\/cards/);
if (isTitleCardsPage && typeof applyTitleMissingMode === 'function') {
applyTitleMissingMode();
}
}
async function checkOnline() {
if (!cfg.onlineStatus) return;
const candidates = document.querySelectorAll('a[href*="/user/"]:not([data-rem-online])');
for (const el of candidates) {
if (el.textContent.includes("Показать") || el.closest('.cs-comments-section, [data-sentry-component="ActivityItemCard"]')) {
el.setAttribute('data-rem-online', 'skip'); continue;
}
const m = el.getAttribute('href').match(/\/user\/(\d+)/);
if (!m) { el.setAttribute('data-rem-online', 'skip'); continue; }
const uid = m[1], av = el.querySelector('[data-slot="avatar"], .relative.shrink-0');
if (!av) { el.setAttribute('data-rem-online', 'skip'); continue; }
el.setAttribute('data-rem-online', '1');
if (Manager.onlineCache.has(uid)) {
const c = Manager.onlineCache.get(uid);
if (Date.now() - c.time < 600000) { drawDot(av, c.online); continue; }
}
if (Manager.onlineProcessing.has(uid)) continue;
Manager.onlineProcessing.add(uid);
Manager.req(`${API_DOMAIN}/api/v2/users/${uid}/`).then(data => {
const status = data ? !!data.is_online : false;
Manager.onlineCache.set(uid, { online: status, time: Date.now() });
Manager.onlineProcessing.delete(uid);
drawDot(av, status);
});
}
}
function drawDot(container, isOnline) {
if (container.querySelector('.rem-online-dot')) return;
const dot = document.createElement('div');
dot.className = `rem-online-dot ${isOnline ? 'online' : 'offline'}`;
if (window.getComputedStyle(container).position === 'static') container.style.position = 'relative';
container.appendChild(dot);
}
async function injectCardStats(dialog, cardId) {
if (!cfg.cardStats) return;
if (dialog.querySelector('.rem-card-stats-row')) return;
const row = document.createElement('div');
row.className = 'rem-card-stats-row';
row.innerHTML = `<div class="rem-stat-bubble owners"><span class="rem-stat-num loading">...</span><span class="rem-stat-lab">Владельцы</span></div><div class="rem-stat-bubble wants"><span class="rem-stat-num loading">...</span><span class="rem-stat-lab">Хотят</span></div><div class="rem-stat-bubble sales"><span class="rem-stat-num loading">...</span><span class="rem-stat-lab">Продают</span></div>`;
const target = dialog.querySelector('.flex.flex-wrap.items-center.justify-center.gap-3');
if (target) target.parentNode.insertBefore(row, target);
const fetchCount = async (url) => {
let total = 0, page = 1;
while (true) {
const data = await Manager.req(`${url}${url.includes('?') ? '&' : '?'}page=${page}`);
if (!data || !data.results) break;
total += data.results.length;
if (!data.next || page >= 40) break;
page++;
await new Promise(r => setTimeout(r, 100));
}
return total;
};
Promise.all([
fetchCount(`${API_DOMAIN}/api/v2/users/have-card/${cardId}/`),
fetchCount(`${API_DOMAIN}/api/v2/inventory/wishes/${cardId}/?wish_type=1`),
fetchCount(`${API_DOMAIN}/api/v2/inventory/wishes/${cardId}/?wish_type=2`),
]).then(([o, w, s]) => {
const oN = row.querySelector('.owners .rem-stat-num'),
wN = row.querySelector('.wants .rem-stat-num'),
sN = row.querySelector('.sales .rem-stat-num');
if (oN) { oN.innerText = o; oN.classList.remove('loading'); }
if (wN) { wN.innerText = w; wN.classList.remove('loading'); }
if (sN) { sN.innerText = s; sN.classList.remove('loading'); }
});
}
let titleMissingMode = false;
let titleCacheOwned = 0, titleCacheTotal = 0, titleCacheStats = null;
let isCalculating = false;
let lastSlug = '';
function applyTitleMissingMode() {
let grid = document.querySelector('.grid.gap-3');
if (!grid) return;
let ph = document.getElementById('rem-all-collected-ph');
if (titleMissingMode && titleCacheOwned > 0 && titleCacheOwned === titleCacheTotal) {
grid.style.display = 'none';
if (!ph) {
ph = document.createElement('div');
ph.id = 'rem-all-collected-ph';
ph.innerHTML = '✨ Все карты собраны! ✨';
ph.style.cssText = 'height: 50vh; display: flex; align-items: center; justify-content: center; font-size: 24px; color: #71717a;';
grid.parentElement.insertBefore(ph, grid.nextSibling);
}
ph.style.display = 'flex';
} else {
grid.style.display = '';
if (titleMissingMode) {
grid.style.minHeight = '100vh';
grid.style.alignContent = 'start';
} else {
grid.style.minHeight = '';
grid.style.alignContent = '';
}
if (ph) ph.style.display = 'none';
document.querySelectorAll('img[src*="/media/card-item/"]').forEach(img => {
const fname = Manager.getFilename(img.src);
const have = Manager.has(fname);
const cardCont = img.closest('.grid > div') || img.closest('[data-sentry-component="CardItem"]') || img.parentElement.parentElement;
if (cardCont) {
if (titleMissingMode && have) {
cardCont.style.display = 'none';
} else if (cardCont.style.display === 'none') {
cardCont.style.display = '';
}
}
});
}
}
function buildProgressHtml(owned, total, rankStats) {
if (titleMissingMode) {
let badgesHtml = '';
if (rankStats) {
RANK_ORDER.forEach(rankKey => {
const st = rankStats[rankKey];
if (st && st.total > 0) {
const r = RANK_MAP[rankKey] || { name: rankKey.toUpperCase(), color: '#a1a1aa' };
const miss = st.total - st.owned;
if (miss > 0) {
badgesHtml += `<div class="rem-rank-badge" style="color:${r.color};border-color:${r.color};">${r.name}: <span style="color:#fff;margin-left:3px;">-${miss}</span></div>`;
}
}
});
}
return `
<div class="rem-progress-header" style="justify-content:space-between;align-items:center;">
<div class="rem-progress-text">Недостающие карты: <span>${total - owned}</span></div>
<button class="rem-ex-btn secondary" id="rem-missing-toggle" style="height:28px;padding:0 12px;font-size:11px;">🔍 Показать все</button>
</div>
<div class="rem-rank-stats">${badgesHtml || '<div style="color:#71717a;font-size:12px;">Все карты собраны! 🎉</div>'}</div>
`;
} else {
let percentNum = 0, percentStr = "0%";
if (total > 0 && owned > 0) {
const raw = (owned / total) * 100;
if (owned === total) { percentNum = 100; percentStr = "100%"; }
else if (raw < 1) { percentStr = raw.toFixed(1) + "%"; percentNum = raw; }
else if (raw > 99) { percentStr = raw.toFixed(1) + "%"; percentNum = raw; }
else { percentNum = Math.floor(raw); percentStr = percentNum + "%"; }
}
let badgesHtml = '';
if (rankStats) {
RANK_ORDER.forEach(rankKey => {
if (rankStats[rankKey] && rankStats[rankKey].total > 0) {
const r = RANK_MAP[rankKey] || { name: rankKey.toUpperCase(), color: '#a1a1aa' };
const st = rankStats[rankKey];
badgesHtml += `<div class="rem-rank-badge" style="color:${r.color};border-color:${r.color};">${r.name}: <span style="color:#fff;margin-left:3px;">${st.owned} / ${st.total}</span></div>`;
}
});
}
return `
<div class="rem-progress-header">
<div class="rem-progress-text">Собрано карт: <span>${owned}</span> / ${total}</div>
<div style="display:flex;align-items:center;gap:12px;">
<button class="rem-ex-btn secondary" id="rem-missing-toggle" style="height:28px;padding:0 12px;font-size:11px;">👁️ Скрыть полученные</button>
<div id="rem-percent">${percentStr}</div>
</div>
</div>
<div class="rem-progress-track"><div class="rem-progress-fill" style="width:${percentNum}%"></div></div>
<div class="rem-rank-stats">${badgesHtml}</div>
`;
}
}
function createProgressBox() {
const box = document.createElement('div');
box.id = 'rem-progress-box';
box.className = 'rem-progress-box';
box.innerHTML = buildProgressHtml(0, 0, null);
return box;
}
function updateProgressBox(box, owned, total, rankStats) {
titleCacheOwned = owned;
titleCacheTotal = total;
titleCacheStats = rankStats;
box.innerHTML = buildProgressHtml(owned, total, rankStats);
const btn = box.querySelector('#rem-missing-toggle');
if (btn) {
btn.onclick = () => {
titleMissingMode = !titleMissingMode;
updateProgressBox(box, titleCacheOwned, titleCacheTotal, titleCacheStats);
applyTitleMissingMode();
};
}
}
async function injectTitleProgress() {
if (!cfg.titleProgress) return;
const match = location.pathname.match(/^\/manga\/([\w-]+)\/cards/);
if (!match) {
const oldBox = document.getElementById('rem-progress-box');
if (oldBox) oldBox.remove();
isCalculating = false; lastSlug = '';
return;
}
if (!Manager.loaded || isCalculating) return;
const grid = document.querySelector('.grid.gap-3');
if (!grid) return;
const slug = match[1];
if (lastSlug !== slug) {
const oldBox = document.getElementById('rem-progress-box');
if (oldBox) oldBox.remove();
lastSlug = slug;
titleMissingMode = false;
}
if (document.getElementById('rem-progress-box')) return;
isCalculating = true;
const box = createProgressBox();
grid.parentNode.insertBefore(box, grid);
try {
const titleData = await Manager.req(`${API_DOMAIN}/api/titles/${slug}/`);
if (!titleData || !titleData.content || !titleData.content.id) { isCalculating = false; return; }
const titleId = titleData.content.id;
let totalCards = 0, ownedCards = 0, rankStats = {}, page = 1, run = true;
while (run) {
if (!location.pathname.includes(slug)) { isCalculating = false; return; }
const data = await Manager.req(`${API_DOMAIN}/api/inventory/${titleId}/cards/?count=100&ordering=rank&page=${page}`);
if (!data) { run = false; break; }
const list = data.results || data.content || [];
if (!list.length) { run = false; break; }
list.forEach(item => {
totalCards++;
const c = item.card || item;
const rank = c.rank || 'unknown';
if (!rankStats[rank]) rankStats[rank] = { total: 0, owned: 0 };
rankStats[rank].total++;
if (c.cover) {
const h = Manager.getFilename(c.cover.high);
const m = Manager.getFilename(c.cover.mid);
if ((h && Manager.has(h)) || (m && Manager.has(m))) {
ownedCards++;
rankStats[rank].owned++;
}
}
});
if (!data.next) run = false; else page++;
await new Promise(r => setTimeout(r, 100));
}
const finalBox = document.getElementById('rem-progress-box');
if (finalBox) updateProgressBox(finalBox, ownedCards, totalCards, rankStats);
} catch (e) { console.error(e); }
isCalculating = false;
}
function scanForDialog() {
const dialog = document.querySelector('[role="dialog"]');
if (!dialog) return;
const cardLink = dialog.querySelector('a[href^="/card/"]');
if (cardLink && !dialog.dataset.statsInjected) {
const cid = cardLink.getAttribute('href').match(/card\/(\d+)/)?.[1];
if (cid) { dialog.dataset.statsInjected = cid; injectCardStats(dialog, cid); }
}
const mangaLink = dialog.querySelector('a[href^="/manga/"]');
if (mangaLink && !mangaLink.dataset.fixHook) {
mangaLink.dataset.fixHook = "true";
mangaLink.dataset.origHref = mangaLink.getAttribute('href');
const isActive = cfg.fixMode;
const btn = document.createElement('span');
btn.className = `rem-toggle ${isActive ? 'on' : 'off'}`;
btn.innerText = `FIX: ${isActive ? 'ON' : 'OFF'}`;
if (mangaLink.nextSibling) mangaLink.parentNode.insertBefore(btn, mangaLink.nextSibling);
else mangaLink.parentNode.appendChild(btn);
btn.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
const newState = !cfg.fixMode;
saveSetting('fixMode', newState);
btn.className = `rem-toggle ${newState ? 'on' : 'off'}`;
btn.innerText = `FIX: ${newState ? 'ON' : 'OFF'}`;
if (!newState) mangaLink.href = mangaLink.dataset.origHref;
});
mangaLink.addEventListener('click', async (e) => {
if (cfg.fixMode) {
e.preventDefault(); e.stopPropagation();
if (!cardLink) return;
const m = cardLink.getAttribute('href').match(/card\/(\d+)/);
if (!m) return;
btn.innerText = "⏳";
const cardData = await Manager.req(`${API_DOMAIN}/api/inventory/cards/${m[1]}/`);
if (cardData && cardData.title && cardData.title.id) {
const titleId = cardData.title.id;
const titleName = cardData.title.main_name || "Тайтл";
const slug = mangaLink.dataset.origHref.split('/')[2];
btn.innerText = "FIX: ON";
window.location.href = `${SITE_DOMAIN}/manga/${slug}?fix_id=${titleId}&fix_name=${encodeURIComponent(titleName)}`;
} else { btn.innerText = "ERR"; }
}
});
}
}
async function runFix(fid, rawName) {
if (activeObserver) { activeObserver.disconnect(); activeObserver = null; }
document.documentElement.classList.add('rem-checking');
const l = document.createElement('div'); l.id = 'rem-loader'; l.innerText = "Загрузка данных (0%)...";
if (!document.body) await new Promise(r => addEventListener('DOMContentLoaded', r));
document.body.appendChild(l);
const slug = location.pathname.split('/')[2];
const status = await Manager.req(`${API_DOMAIN}/api/titles/${slug}/`);
l.remove(); document.documentElement.classList.remove('rem-checking');
if (status && status.content) {
const url = new URL(window.location);
url.searchParams.delete('fix_id'); url.searchParams.delete('fix_name');
window.history.replaceState({}, '', url);
return;
}
const tName = rawName ? decodeURIComponent(rawName) : "Title";
let main = document.querySelector('main');
let attempts = 0;
while (!main && attempts < 30) { await new Promise(r => setTimeout(r, 50)); main = document.querySelector('main'); attempts++; }
if (!main) main = document.body;
document.body.classList.add('rem-active');
const box = document.createElement('div');
box.id = 'rem-inject'; box.className = "container mx-auto px-4 py-6";
box.innerHTML = `<div style="height:50vh;display:flex;align-items:center;justify-content:center;color:#888;">Загрузка карт...</div>`;
main.appendChild(box);
activeObserver = new MutationObserver(() => {
if (!document.body.classList.contains('rem-active')) document.body.classList.add('rem-active');
if (!document.getElementById('rem-inject')) { const m = document.querySelector('main') || document.body; m.appendChild(box); }
});
activeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
activeObserver.observe(main, { childList: true });
let allCards = [];
let page = 1, run = true;
while (run) {
box.innerHTML = `<div style="height:50vh;display:flex;align-items:center;justify-content:center;color:#888;">Загрузка стр. ${page}...</div>`;
const data = await Manager.req(`${API_DOMAIN}/api/inventory/${fid}/cards/?count=100&ordering=rank&page=${page}`);
if (!data) { run = false; break; }
const list = data.results || data.content || [];
if (!list.length) { run = false; break; }
allCards = allCards.concat(list);
if (data.next) {
page++;
} else if (data.props && data.props.total_pages && page < data.props.total_pages) {
page++;
} else if (data.count && page * 100 < data.count) {
page++;
} else {
run = false;
}
if (run) await new Promise(r => setTimeout(r, 20));
}
if (!allCards.length) { box.innerHTML = `<div style="text-align:center;padding:50px">Пусто</div>`; return; }
let ownedInTitle = 0, rankStats = {};
const grid = document.createElement('div');
grid.className = 'rem-grid';
allCards.forEach(item => {
const c = item.card || item;
const rank = c.rank || 'unknown';
if (!rankStats[rank]) rankStats[rank] = { total: 0, owned: 0 };
rankStats[rank].total++;
let have = false;
if (c.cover) {
const h = Manager.getFilename(c.cover.high);
const m = Manager.getFilename(c.cover.mid);
if ((h && Manager.has(h)) || (m && Manager.has(m))) have = true;
}
if (have) { ownedInTitle++; rankStats[rank].owned++; }
const imgUrl = c.cover?.high ? `${SITE_DOMAIN}${c.cover.high}` : (c.cover?.mid ? `${SITE_DOMAIN}${c.cover.mid}` : null);
const el = document.createElement('div');
el.className = 'rem-card';
if (imgUrl) {
if (imgUrl.endsWith('.webm') || imgUrl.endsWith('.mp4')) {
el.innerHTML = `<video src="${imgUrl}#t=1.0" muted playsinline preload="metadata" style="width:100%;height:100%;object-fit:cover;pointer-events:none;"></video>`;
} else {
el.innerHTML = `<img src="${imgUrl}" loading="lazy">`;
}
} else {
el.innerHTML = '';
}
el.onclick = () => showModal(c, tName);
if (have && cfg.ownBadge) {
if (window.getComputedStyle(el).position === 'static') el.style.position = 'relative';
el.appendChild(createCheck());
}
grid.appendChild(el);
});
const progressBox = createProgressBox();
updateProgressBox(progressBox, ownedInTitle, allCards.length, rankStats);
progressBox.style.marginBottom = '20px';
box.innerHTML = `
<div class="rem-fix-header">
<h1 class="rem-fix-title">${tName}</h1>
<div class="rem-fix-meta">
<span>ID: ${fid}</span><span class="rem-sep">|</span>
<span>${allCards.length} шт</span><span class="rem-sep">|</span>
<span style="color:#22c55e;">● Fix Active</span>
</div>
</div>
`;
box.appendChild(progressBox);
box.appendChild(grid);
}
function checkNavigation() {
const p = new URLSearchParams(location.search);
if (!p.get('fix_id')) {
if (activeObserver) { activeObserver.disconnect(); activeObserver = null; }
if (document.body.classList.contains('rem-active')) document.body.classList.remove('rem-active');
const inj = document.getElementById('rem-inject'); if (inj) inj.remove();
const load = document.getElementById('rem-loader'); if (load) load.remove();
document.documentElement.classList.remove('rem-checking');
} else if (!document.body.classList.contains('rem-active') && !document.getElementById('rem-loader')) {
runFix(p.get('fix_id'), p.get('fix_name'));
}
}
function getAuthorInfo(c) {
const u = c.author || c.user || c.upload_user || c.owner || c.publisher || c.creator;
if (u && (u.username || u.name)) return { name: u.username || u.name, link: u.id ? `/user/${u.id}/about` : '#' };
return { name: "Неизвестен", link: "#" };
}
function showModal(c, tName) {
const imgUrl = c.cover?.high ? `${SITE_DOMAIN}${c.cover.high}` : '';
const isVid = imgUrl && (imgUrl.endsWith('.webm') || imgUrl.endsWith('.mp4'));
const imgHtml = isVid
? `<video src="${imgUrl}#t=1.0" muted playsinline preload="metadata" style="width:100%;height:100%;object-fit:cover;pointer-events:none;"></video>`
: `<img src="${imgUrl}">`;
const name = c.name || c.character?.name || "?";
const cid = c.character?.id || 0;
const uInfo = getAuthorInfo(c);
const likes = c.likes_count || 0;
const cLink = c.id ? `/card/${c.id}` : '#';
const over = document.createElement('div'); over.className = 'rem-over';
over.onclick = e => { if (e.target === over) over.remove(); };
let titleUrl = `/manga/${location.pathname.split('/')[2]}`;
const p = new URLSearchParams(location.search);
if (p.get('fix_id')) titleUrl += `?fix_id=${p.get('fix_id')}&fix_name=${p.get('fix_name')}`;
else { const slug = c.title ? c.title.dir : '#'; if (slug !== '#') titleUrl = `/manga/${slug}`; }
over.innerHTML = `
<div class="rem-box">
<button class="rem-close" id="x"><svg width="20" height="20" viewBox="0 0 20 20" stroke="currentColor" stroke-width="1.5" fill="none"><path d="M6 14L14 6M14 14L6 6" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<div class="rem-box-img">${imgHtml}</div>
<div class="rem-info">
<div><a href="${titleUrl}" class="rem-lnk-m" target="_blank">${tName} ↗</a>${p.get('fix_id') ? '<span class="rem-toggle on" style="font-size:10px;padding:2px 4px;margin-left:5px">FIX: ON</span>' : ''}</div>
<a href="/character/${cid}" class="rem-lnk-c" target="_blank">${name} ↗</a>
</div>
<div class="rem-acts"><button class="rem-pill">Like ${likes}</button></div>
<div class="rem-sub"><a href="${uInfo.link}" target="_blank" class="rem-s-btn">Автор: ${uInfo.name}</a><a href="${cLink}" target="_blank" class="rem-s-btn">Пользователи</a></div>
</div>`;
document.body.appendChild(over);
document.getElementById('x').onclick = () => over.remove();
}
function getProfileInjectionPoint() {
const isMobile = window.innerWidth <= 768;
const cont = document.querySelector('.cs-layout-avatar-container');
if (isMobile) {
const mobileRow = document.querySelector('.flex.flex-row.gap-2.max-sm\\:w-full');
if (mobileRow) return { targetCol: mobileRow.parentElement, refNode: mobileRow };
if (cont) {
const guild = cont.nextElementSibling;
const refNode = (guild && !guild.classList.contains('mt-auto')) ? guild : cont;
return { targetCol: cont.parentElement, refNode: refNode };
}
} else {
if (cont) {
const guild = cont.nextElementSibling;
const refNode = (guild && !guild.classList.contains('mt-auto')) ? guild : cont;
return { targetCol: cont.parentElement, refNode: refNode };
}
const desktopRow = document.querySelector('.flex.flex-row.gap-2.max-sm\\:w-full');
if (desktopRow) return { targetCol: desktopRow.parentElement, refNode: desktopRow };
}
const btns = Array.from(document.querySelectorAll('button'));
const fbtn = btns.find(b => b.textContent && (b.textContent.includes('друзья') || b.textContent.includes('сообщение')));
if (fbtn && fbtn.parentElement) return { targetCol: fbtn.parentElement.parentElement, refNode: fbtn.parentElement };
return null;
}
async function injectScanButton() {
if (!cfg.scanButton) return;
const m = location.pathname.match(/^\/user\/(\d+)/);
if (!m) return;
const uid = m[1];
if (uid === Manager.userId) return;
const pt = getProfileInjectionPoint();
if (!pt) return;
const { targetCol, refNode } = pt;
if (document.querySelector('.rem-scan-btn')) return;
const inList = isInScanList(uid);
const btn = document.createElement('button');
btn.className = `rem-scan-btn ${inList ? 'active' : ''}`;
btn.innerHTML = `
<span class="rem-scan-check">
<svg viewBox="0 0 12 12" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="10 3 5 9 2 6"></polyline></svg>
</span>
<span class="rem-scan-label">${inList ? 'В списке сканирования' : 'Добавить в сканирование'}</span>
`;
let customWrap = targetCol.querySelector('.rem-custom-actions-wrap');
if (!customWrap) {
customWrap = document.createElement('div');
customWrap.className = 'rem-custom-actions-wrap';
customWrap.style.cssText = 'width:100%; display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:8px; margin-bottom:8px;';
if (refNode && refNode.nextSibling) targetCol.insertBefore(customWrap, refNode.nextSibling);
else targetCol.appendChild(customWrap);
}
customWrap.appendChild(btn);
btn.onclick = () => {
const currentlyInList = isInScanList(uid);
if (currentlyInList) {
removeFromScanList(uid);
btn.classList.remove('active');
btn.querySelector('.rem-scan-label').textContent = 'Добавить в сканирование';
} else {
let username = `User_${uid}`;
const titleText = document.title.split(/ [|—\-/] /)[0].trim();
const nameEl = document.querySelector('h1, .text-2xl, [class*="username"]');
const _isSiteTitle = (t) => t === 'Remanga' || /реманга/i.test(t) || /xn--80aaig9ahr/i.test(t);
if (titleText && !_isSiteTitle(titleText) && !titleText.includes('Ошибка')) {
username = titleText.replace(/^Профиль\s+/i, '').replace(/#\d+$/, '').trim();
} else if (nameEl) {
const txt = nameEl.textContent.trim();
if (txt && txt.length < 60) username = txt.replace(/#\d+$/, '').trim();
}
addToScanList(uid, username);
btn.classList.add('active');
btn.querySelector('.rem-scan-label').textContent = 'В списке сканирования';
Manager.req(`${API_DOMAIN}/api/v2/users/${uid}/`).then(udata => {
if (!udata) return;
let apiName = udata.username || (udata.content && udata.content.username) || (udata.user && udata.user.username);
if (apiName) apiName = apiName.replace(/#\d+$/, '').trim();
if (apiName && apiName !== username) {
const list = getScanList();
const entry = list.find(u => String(u.id) === String(uid));
if (entry) { entry.username = apiName; saveScanList(list); }
}
}).catch(() => { });
}
};
}
async function injectStats() {
if (!cfg.profileStats && !cfg.deckStats) return;
const m = location.pathname.match(/^\/user\/(\d+)/); if (!m) return;
const uid = m[1];
const pt = getProfileInjectionPoint();
if (!pt) return;
const { targetCol, refNode } = pt;
if (document.querySelector('.rem-profile-stat-badge')) return;
const badge = document.createElement('div');
badge.className = 'rem-profile-stat-badge rem-counting';
badge.innerHTML = cfg.profileStats ? `🎴 Создал карт: <strong>...</strong>` : `📦 Загрузка...`;
let customWrap = targetCol.querySelector('.rem-custom-actions-wrap');
if (!customWrap) {
customWrap = document.createElement('div');
customWrap.className = 'rem-custom-actions-wrap';
customWrap.style.cssText = 'width:100%; display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:8px; margin-bottom:8px;';
if (refNode && refNode.nextSibling) targetCol.insertBefore(customWrap, refNode.nextSibling);
else targetCol.appendChild(customWrap);
}
badge.style.marginTop = '0';
customWrap.appendChild(badge);
let cVerb = "Создал";
let oVerb = "Открыл";
try {
const userRes = await Manager.req(`${API_DOMAIN}/api/v2/users/${uid}/`);
if (userRes) {
let s = undefined;
if (userRes.sex !== undefined) s = userRes.sex;
else if (userRes.content && userRes.content.sex !== undefined) s = userRes.content.sex;
else if (userRes.user && userRes.user.sex !== undefined) s = userRes.user.sex;
if (s !== undefined) {
const sex = Number(s);
if (sex === 0) { cVerb = "Создало"; oVerb = "Открыло"; }
if (sex === 1) { cVerb = "Создал"; oVerb = "Открыл"; }
if (sex === 2) { cVerb = "Создала"; oVerb = "Открыла"; }
}
}
} catch (e) { }
let deckPart = "";
if (cfg.deckStats) {
try {
const res = await Manager.req(`${API_DOMAIN}/api/v2/inventory/decks/??is_opened=false&user_id=${uid}&count=1`);
if (res) {
const count = res.count || 0;
deckPart = ` | 📦 ${oVerb} паков: <strong>${count}</strong>`;
}
} catch (e) { }
}
if (!cfg.profileStats) {
badge.classList.remove('rem-counting');
badge.innerHTML = deckPart ? deckPart.replace(/^ \| /, '') : `📦 ${oVerb} паков: 0`;
return;
}
let total = 0, page = 1, run = true;
while (run) {
badge.innerHTML = `🎴 ${cVerb} карт: <strong>${total}...</strong>${deckPart}`;
const r = await Manager.req(`${API_DOMAIN}/api/inventory/catalog/?author=${uid}&count=30&ordering=-rank&page=${page}`);
if (!r) break;
const list = r.results || [];
if (!list.length) break;
total += list.length;
if (!r.next) run = false; else { page++; await new Promise(r => setTimeout(r, 150)); }
}
badge.classList.remove('rem-counting');
badge.innerHTML = `🎴 ${cVerb} карт: <strong>${total}</strong>${deckPart}`;
}
let filterMapCache = null;
async function getFilterMap() {
if (filterMapCache) return filterMapCache;
const res = await Manager.req('api/forms/titles/?is_without_ex=true');
const map = { genres: {}, categories: {} };
if (res && res.content) {
if (res.content.genres) res.content.genres.forEach(g => map.genres[g.name.toLowerCase()] = g.id);
if (res.content.categories) res.content.categories.forEach(c => map.categories[c.name.toLowerCase()] = c.id);
}
const fallbackCategories = {
"веб": 5, "в цвете": 6, "зомби": 14, "демоны": 15, "кулинария": 16, "медицина": 17, "культивация": 18, "зверолюди": 19,
"магия": 22, "горничные": 23, "средневековье": 25, "антигерой": 26,
"ниндзя": 30, "офисные работники": 31, "полиция": 32, "самураи": 33,
"видеоигры": 35, "криминал": 36, "обратный гарем": 40,
"выживание": 41, "путешествия во времени": 43, "боги": 45, "алхимия": 47, "ангелы": 48,
"антиутопия": 49, "апокалипсис": 50, "армия": 51, "артефакты": 52, "борьба за власть": 54,
"будущее": 55, "владыка демонов": 57, "волшебные существа": 59, "воспоминания из другого мира": 60,
"геймеры": 61, "гильдии": 62, "гг женщина": 63, "гг мужчина": 64, "дружба": 67, "ранги силы": 68,
"жестокий мир": 69, "животные компаньоны": 70, "игровые элементы": 73, "космос": 76,
"магическая академия": 78, "месть": 79, "навыки": 80, "наёмники": 81, "насилие / жестокость": 82,
"нежить": 83, "подземелья": 86, "политика": 87, "разумные расы": 88, "рыцари": 90, "система": 91,
"скрытие личности": 93, "спасение мира": 94, "супер герои": 95, "учитель / ученик": 96,
"шантаж": 99, "тупой гг": 109, "гг имба": 110, "умный гг": 111, "управление территорией": 114,
"исекай": 115, "аристократия": 117, "амнезия": 121, "бои на мечах": 122, "гг не человек": 123,
"упоротость": 124, "грузовик-сан": 125, "учебное заведение": 126, "аниме": 127,
"лоли": 108, "хикикомори": 21, "реинкарнация": 13, "монстры": 38
};
const fallbackGenres = {
"экшен": 2, "боевые искусства": 3, "гарем": 5, "героическое фэнтези": 7, "детектив": 8,
"дзёсэй": 9, "додзинси": 10, "драма": 11, "история": 13, "киберпанк": 14,
"элементы юмора": 16, "меха": 18, "мистика": 19, "научная фантастика": 20,
"повседневность": 21, "постапокалиптика": 22, "приключения": 23, "психология": 24,
"романтика": 25, "сверхъестественное": 27, "сёдзё": 28, "сёнэн": 30,
"спорт": 32, "сэйнэн": 33, "трагедия": 34, "триллер": 35, "ужасы": 36,
"фантастика": 37, "фэнтези": 38, "школьники": 39, "этти": 40, "эротика": 42,
"комедия": 50, "мурим": 51
};
Object.assign(map.categories, fallbackCategories);
Object.assign(map.genres, fallbackGenres);
try {
const pagesToCrawl = [
'api/v2/search/catalog/?count=30&ordering=-score&page=1&unstrict_search_fields=categories',
'api/v2/search/catalog/?categories=47&count=30&ordering=-score&page=1',
'api/v2/search/catalog/?categories=127&count=30&ordering=-score&page=1'
];
for (const p of pagesToCrawl) {
const data = await Manager.req(p);
if (data && data.results) {
data.results.forEach(m => {
if (m.genres) m.genres.forEach(g => map.genres[g.name.toLowerCase()] = g.id);
if (m.categories) m.categories.forEach(c => map.categories[c.name.toLowerCase()] = c.id);
});
}
}
} catch (e) { }
filterMapCache = map;
return map;
}
function ruToSlug(str) {
const m = { 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'e', 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch', 'ъ': '', 'ы': '', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya' };
return str.toLowerCase().replace(/[^а-яёa-z0-9]/g, '').split('').map(c => m[c] !== undefined ? m[c] : c).join('');
}
async function executeBPTask(card) {
const t = card.textContent.toLowerCase();
const uid = Manager.userId || '';
const go = (path) => {
Manager.showStatus('⏳ Перенаправление...', true);
setTimeout(() => { Manager.showStatus('', false); window.open((path.startsWith('http') ? path : SITE_DOMAIN + path), '_blank'); }, 500);
};
const goX = async (fn) => {
Manager.showStatus('⏳ Подготовка редиректа...', true);
try {
const path = await fn();
go(path);
} catch (e) { go('/'); }
};
if (t.includes('разнообразие жанров')) go('/manga/?count_chapters=1-&ordering=-score&unstrict_search_fields=genres%2Ccategories');
else if (t.includes('новые карточки')) go('/deck/10/open');
else if (t.includes('путешествие по мирам')) go('/manga?count_chapters=300-&ordering=-score&unstrict_search_fields=genres%2Ccategories');
else if (t.includes('шпион') || t.includes('чужой профиль') || t.includes('дружеский жест')) {
const rndId = Math.floor(Math.random() * 2000000) + 1;
go(`/user/${rndId}/about`);
}
else if (t.includes('личное')) go(`/user/${uid}/about`);
else if (
t.includes('волна лайков') ||
t.includes('читательский марафон') || t.includes('погружение в истории') ||
t.includes('даритель любви')
) go('/manga?count_chapters=300-&ordering=-score&unstrict_search_fields=genres%2Ccategories');
else if (t.includes('вперёд за покупками') || t.includes('вперед за покупками')) go('/customization/feed/frame?ordering=cost');
else if (t.includes('купи её') || t.includes('коллекционер историй') || t.includes('потрать тикеты')) go('/manga/one_hundred_to_make_god/chapters?ordering=index');
else if (t.includes('оценка эксперта')) go('/manga/?count_chapters=10-&exclude_bookmarks=51891154%2C51891155%2C51891156%2C51891160&ordering=-score&unstrict_search_fields=genres%2Ccategories');
else if (t.includes('оно похоже')) {
const similarLinks = [
'/manga/pinaty/similar',
'/manga/the-greatest-estate-designer_/similar',
'/manga/trash-of-the-counts-family/similar',
'/manga/one_punch_man_/similar',
'/manga/omniscient-reader_/similar',
'/manga/solo-leveling_/similar',
'/manga/sss-level-hunter_/similar'
];
go(similarLinks[Math.floor(Math.random() * similarLinks.length)]);
}
else if (t.includes('одобрительный отклик') || t.includes('оценка мнения')) go('/manga/pinaty/comments');
else if (t.includes('моё мнение') || t.includes('мое мнение') || t.includes('ответ на мнение')) {
const commentLinks = [
'/manga/pinaty/comments',
'/manga/god-of-ad-libs/comments',
'/manga/i-was-immediately-mistaken-for-a-genius-actor_/comments',
'/manga/omniscient-reader_/comments',
'/manga/regression-instruction_/comments',
'/manga/the-pick-me-up_/comments'
];
go(commentLinks[Math.floor(Math.random() * commentLinks.length)]);
}
else if (t.includes('новый образ')) go(`/user/${uid}/inventory?type=customization&shopType=frames`);
else if (t.includes('найди пару')) go(`/user/${uid}/mini-games`);
else if (t.includes('целое') || t.includes('гильди')) go('/guild');
else if (t.includes('улучши') || t.includes('апгрейд') || t.includes('создай')) go(`/user/inventory/cards-upgrade?ordering=-id&card__rank=rank_f&is_favorite=false`);
else if (t.includes('обменяемся') || t.includes('обмен')) {
goX(async () => {
let allFriends = [];
const p1 = await Manager.req(`${API_DOMAIN}/api/v2/users/${uid}/friends/?count=20&page=1`);
if (p1 && p1.results) allFriends.push(...p1.results);
const p2 = await Manager.req(`${API_DOMAIN}/api/v2/users/${uid}/friends/?count=20&page=2`);
if (p2 && p2.results) allFriends.push(...p2.results);
if (allFriends.length > 0) {
const rnd = allFriends[Math.floor(Math.random() * allFriends.length)];
return `/user/${rnd.id}/create/exchange`;
}
const rndId = Math.floor(Math.random() * 2000000) + 1;
return `/user/${rndId}/create/exchange`;
});
}
else if (t.includes('золот') || t.includes('баланс')) go(`/user/${uid}/balance`);
else if (t.includes('категори') || t.includes('жанр')) {
goX(async () => {
const map = await getFilterMap();
if (!map) return '/manga';
const allBpButtons = Array.from(document.querySelectorAll('button'))
.filter(b => b.textContent && b.textContent.trim().toLowerCase() === 'получить');
const taskButtons = allBpButtons.filter((b, idx) => {
let card = b;
for (let i = 0; i < 6; i++) {
if (!card.parentElement) break;
card = card.parentElement;
if (card.textContent.length > 50 && card.querySelector('.font-bold, .font-semibold, h1, h2, h3, h4, h5, b, strong')) break;
}
if (!card) return false;
const t = card.textContent.toLowerCase();
return t.includes('категори') || t.includes('жанр');
});
const knownTags = [...Object.keys(map.genres), ...Object.keys(map.categories)]
.filter(t => t.length >= 2)
.sort((a, b) => b.length - a.length);
let taskRequirements = [];
taskButtons.forEach((b, idx) => {
let card = b;
for (let i = 0; i < 6; i++) {
if (!card.parentElement) break;
card = card.parentElement;
if (card.textContent.length > 50 && card.querySelector('.font-bold, .font-semibold, h1, h2, h3, h4, h5, b, strong')) break;
}
let taskText = '';
if (card) {
const t = card.textContent.toLowerCase();
if (t.includes('категори') || t.includes('жанр')) {
const progressMatch = card.textContent.replace(/\s+/g, '').match(/(\d+)\/(\d+)/);
const done = progressMatch && parseInt(progressMatch[1], 10) >= parseInt(progressMatch[2], 10);
if (done) {
} else {
taskText = t;
}
}
}
if (taskText) {
const foundItems = knownTags.filter(tag => taskText.includes(tag));
if (foundItems.length > 0) {
taskRequirements.push(foundItems);
}
}
});
if (taskRequirements.length === 0) {
if (t.includes('разнообразие жанров')) return '/manga/?count_chapters=1-&ordering=-score&unstrict_search_fields=genres%2Ccategories';
return '/manga';
}
let validReqs = taskRequirements.map(reqList => reqList.filter(item => map.genres[item] || map.categories[item])).filter(list => list.length > 0);
const selectedTags = new Set();
const selectedCategoryIds = [];
const selectedGenreIds = [];
validReqs.forEach(reqList => {
const shuffled = [...reqList];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
let item = shuffled.find(tag => {
const id = map.genres[tag] || map.categories[tag];
return id && !selectedCategoryIds.includes(id) && !selectedGenreIds.includes(id);
});
if (!item) item = shuffled[0];
if (item) {
selectedTags.add(item);
if (map.genres[item]) selectedGenreIds.push(map.genres[item]);
else if (map.categories[item]) selectedCategoryIds.push(map.categories[item]);
}
});
let query = '?ordering=-score&unstrict_search_fields=';
if (selectedCategoryIds.length) query += `&categories=${selectedCategoryIds.join(',')}`;
if (selectedGenreIds.length) query += `&genres=${selectedGenreIds.join(',')}`;
query += '&count_chapters=1-';
return `/manga/${query}`;
});
}
else if (t.includes('похоже на триплет')) go('/user/inventory/cards-upgrade?ordering=-id&card__rank=rank_f&is_favorite=false');
else go('/');
}
function injectBattlepassExecuteButtons() {
if (!location.pathname.includes('/user/battlepass/tasks')) return;
document.querySelectorAll('.rem-bp-exec-btn').forEach(execBtn => {
let card = execBtn;
for (let i = 0; i < 6; i++) {
if (!card.parentElement) break;
card = card.parentElement;
if (card.textContent.length > 50 && card.querySelector('.font-bold, .font-semibold, h1, h2, h3, h4, h5, b, strong')) break;
}
if (!card) { execBtn.remove(); return; }
let hasGetBtn = Array.from(card.querySelectorAll('button')).some(b => b.textContent && b.textContent.trim().toLowerCase() === 'получить');
const textContent = card.textContent.replace(/\s+/g, '');
const progressMatch = textContent.match(/(\d+)\/(\d+)/);
if (!hasGetBtn || (progressMatch && parseInt(progressMatch[1], 10) >= parseInt(progressMatch[2], 10))) {
execBtn.remove();
delete card.dataset.remExecuteInjected;
}
});
const btns = Array.from(document.querySelectorAll('button')).filter(b => b.textContent && b.textContent.trim().toLowerCase() === 'получить');
btns.forEach(btn => {
let card = btn;
for (let i = 0; i < 6; i++) {
if (!card.parentElement) break;
card = card.parentElement;
if (card.textContent.length > 50 && card.querySelector('.font-bold, .font-semibold, h1, h2, h3, h4, h5, b, strong')) break;
}
if (!card) return;
const textContent = card.textContent.replace(/\s+/g, '');
const progressMatch = textContent.match(/(\d+)\/(\d+)/);
if (progressMatch && parseInt(progressMatch[1], 10) >= parseInt(progressMatch[2], 10)) {
return;
}
if (card.dataset.remExecuteInjected) return;
const cardText = card.textContent.toLowerCase();
const noButton = [
'давний знакомый', 'я мы одно целое', 'мы одно целое',
'больше золота', 'найди его', 'найди отличие',
'я всё знаю', 'я все знаю', 'они разные'
];
if (noButton.some(kw => cardText.includes(kw))) return;
card.dataset.remExecuteInjected = "1";
const btnContainer = btn.parentNode;
const executeBtn = document.createElement('button');
executeBtn.className = 'rem-bp-exec-btn cs-button z-0 cursor-pointer group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-medium subpixel-antialiased transition-all tap-highlight-transparent transform-gpu data-[pressed=true]:scale-[0.97] opacity-100 outline-none focus-visible:outline-hidden ring-0 rounded-full px-4 h-9 min-w-9 text-sm bg-primary text-primary-foreground hover:bg-primary/90';
executeBtn.style.marginRight = '8px';
executeBtn.innerHTML = 'Выполнить';
executeBtn.onclick = (e) => {
e.preventDefault(); e.stopPropagation();
executeBPTask(card);
};
btnContainer.style.display = 'flex';
btnContainer.style.alignItems = 'center';
btnContainer.insertBefore(executeBtn, btn);
});
}
async function solveMemoryGame() {
const DELAY_CLICK = 400;
const DELAY_PAIR = 900;
const delay = ms => new Promise(r => setTimeout(r, ms));
const buttons = [...document.querySelectorAll('button.relative.flex')]
.filter(btn => btn.querySelector('img[alt="card"]'));
if (buttons.length < 16) return false;
const fiberKey = Object.keys(buttons[0]).find(k =>
k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')
);
if (!fiberKey) return false;
let fiber = buttons[0][fiberKey];
let cardsData = null;
while (fiber && !cardsData) {
let hook = fiber.memoizedState;
while (hook) {
const val = hook.memoizedState;
if (Array.isArray(val) && val.length === 16 && val[0] && typeof val[0] === 'object' && val[0].symbol !== undefined) {
cardsData = val;
break;
}
if (hook.queue && hook.queue.lastRenderedState) {
const qs = hook.queue.lastRenderedState;
if (Array.isArray(qs) && qs.length === 16 && qs[0] && typeof qs[0] === 'object' && qs[0].symbol !== undefined) {
cardsData = qs;
break;
}
}
hook = hook.next;
}
fiber = fiber.return;
}
if (!cardsData) return false;
const groups = {};
cardsData.forEach((card, i) => {
const key = String(card.symbol);
if (!groups[key]) groups[key] = [];
groups[key].push(i);
});
for (const indices of Object.values(groups)) {
if (indices.length >= 2) {
buttons[indices[0]].click();
await delay(DELAY_CLICK);
buttons[indices[1]].click();
await delay(DELAY_PAIR);
}
}
return true;
}
function injectMemoryGameButton() {
if (!location.pathname.includes('/user/battlepass/games/memory')) return;
if (document.querySelector('.rem-bp-exec-btn[data-rem-memory]')) return;
const headers = document.querySelectorAll('p.cs-text');
let descEl = null;
for (const h of headers) {
if (h.textContent.trim() === 'Игра на память') {
descEl = h.nextElementSibling;
break;
}
}
if (!descEl || descEl.dataset.remMemoryInjected) return;
descEl.dataset.remMemoryInjected = '1';
const btn = document.createElement('button');
btn.className = 'rem-bp-exec-btn cs-button z-0 cursor-pointer group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-medium subpixel-antialiased transition-all tap-highlight-transparent transform-gpu data-[pressed=true]:scale-[0.97] opacity-100 outline-none focus-visible:outline-hidden ring-0 rounded-full px-4 h-9 min-w-9 text-sm bg-primary text-primary-foreground hover:bg-primary/90';
btn.style.marginRight = '8px';
btn.dataset.remMemory = '1';
btn.textContent = 'Выполнить';
btn.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
btn.disabled = true;
btn.textContent = '⏳ Решаю...';
const ok = await solveMemoryGame().catch(() => false);
btn.textContent = ok ? '✅ Решено!' : '⚠️ Ошибка';
setTimeout(() => {
btn.textContent = 'Выполнить';
btn.disabled = false;
}, 3000);
};
descEl.textContent = '';
descEl.appendChild(btn);
}
async function solvePuzzleGame() {
const delay = ms => new Promise(r => setTimeout(r, ms));
const grid = document.querySelector('div.grid[style*="grid-template-columns:repeat(4, 1fr)"]');
if (!grid) return false;
const parseId = (el) => {
const img = el.querySelector('img');
let src = img ? img.src : el.style.backgroundImage;
if (!src || src === 'none') return 0;
const url = src.replace(/url\(["']?|["']?\)/g, "");
const filename = Manager.getFilename(url);
const m = filename.match(/(\d+)/) || url.match(/\/(\d+)\./);
return m ? parseInt(m[1]) : 0;
};
const getSortedCells = () => {
return Array.from(grid.children).slice(0, 16).sort((a, b) => {
const ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
if (Math.abs(ra.top - rb.top) > 5) return ra.top - rb.top;
return ra.left - rb.left;
});
};
let state = [];
for (let attempt = 0; attempt < 20; attempt++) {
state = getSortedCells().map(cell => parseId(cell));
if (new Set(state.filter(v => v !== 0)).size === 15 && state.length === 16) break;
await delay(150);
}
if (new Set(state).size < 16) return false;
const goal = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0];
const gPos = new Array(16); goal.forEach((v, i) => gPos[v] = [i >> 2, i & 3]);
const solve = async (start) => {
const weight = 5.0;
const startTime = performance.now();
const getH = (s) => {
let d = 0;
for (let i = 0; i < 16; i++) {
if (s[i] === 0) continue;
const p = gPos[s[i]];
d += Math.abs(p[0] - (i >> 2)) + Math.abs(p[1] - (i & 3));
}
return d;
};
let heap = [{ s: start, p: [], h: getH(start), g: 0 }];
const push = (v) => {
heap.push(v); let i = heap.length - 1;
while (i > 0) {
let p = (i - 1) >> 1;
if (heap[i].g + heap[i].h * weight < heap[p].g + heap[p].h * weight) {
[heap[i], heap[p]] = [heap[p], heap[i]]; i = p;
} else break;
}
};
const pop = () => {
if (heap.length === 0) return null;
const top = heap[0], last = heap.pop();
if (heap.length > 0) {
heap[0] = last; let i = 0;
while (true) {
let l = (i << 1) + 1, r = (i << 1) + 2, s = i;
if (l < heap.length && heap[l].g + heap[l].h * weight < heap[s].g + heap[s].h * weight) s = l;
if (r < heap.length && heap[r].g + heap[r].h * weight < heap[s].g + heap[s].h * weight) s = r;
if (s === i) break;
[heap[i], heap[s]] = [heap[s], heap[i]]; i = s;
}
}
return top;
};
let v = new Set([start.join(',')]);
while (heap.length > 0) {
if (v.size % 2000 === 0 && performance.now() - startTime > 3000) break;
const c = pop();
if (c.h === 0) return c.p;
const z = c.s.indexOf(0);
[z - 4, z + 4, z - 1, z + 1].forEach(ni => {
if (ni >= 0 && ni < 16 && (Math.abs(ni - z) !== 1 || (ni >> 2) === (z >> 2))) {
const ns = [...c.s];[ns[z], ns[ni]] = [ns[ni], ns[z]];
const key = ns.join(',');
if (!v.has(key)) { v.add(key); push({ s: ns, p: [...c.p, ni], h: getH(ns), g: c.g + 1 }); }
}
});
}
return null;
};
if (state.every((v, i) => v === goal[i])) return true;
const path = await solve(state);
if (!path) return false;
for (const idx of path) {
const cells = getSortedCells();
if (cells[idx]) cells[idx].click();
await delay(150);
}
return true;
}
function injectPuzzleGameButton() {
if (!location.pathname.includes('/user/battlepass/games/puzzle')) return;
if (document.querySelector('.rem-bp-exec-btn[data-rem-puzzle]')) return;
const headers = document.querySelectorAll('h1.cs-text');
let descEl = null;
for (const h of headers) {
if (h.textContent.trim() === 'Собери пазл') {
descEl = h.nextElementSibling;
break;
}
}
if (!descEl || descEl.dataset.remPuzzleInjected) return;
descEl.dataset.remPuzzleInjected = '1';
const btn = document.createElement('button');
btn.className = 'rem-bp-exec-btn cs-button z-0 cursor-pointer group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-medium subpixel-antialiased transition-all tap-highlight-transparent transform-gpu data-[pressed=true]:scale-[0.97] opacity-100 outline-none focus-visible:outline-hidden ring-0 rounded-full px-4 h-9 min-w-9 text-sm bg-primary text-primary-foreground hover:bg-primary/90';
btn.style.marginTop = '12px';
btn.dataset.remPuzzle = '1';
btn.textContent = 'Собрать пазл';
btn.onclick = async (e) => {
e.preventDefault(); e.stopPropagation();
btn.disabled = true;
btn.textContent = '⏳ Собираю...';
const ok = await solvePuzzleGame().catch(() => false);
btn.textContent = ok ? '✅ Готово!' : '⚠️ Обнови страницу';
setTimeout(() => { btn.textContent = 'Собрать пазл'; btn.disabled = false; }, 3000);
};
descEl.parentNode.appendChild(btn);
}
function injectDifferenceGameMod() {
if (!location.pathname.includes('/user/battlepass/games/difference')) return;
const img1 = document.querySelector('img[alt="Найди отличия 1"]');
const img2 = document.querySelector('img[alt="Найди отличия 2"]');
if (!img1 || !img2) return;
const c1 = img1.closest('.relative.cursor-crosshair');
const c2 = img2.closest('.relative.cursor-crosshair');
if (!c1 || !c2) return;
const targetContainer = c1.parentElement;
if (!targetContainer || targetContainer.dataset.remDiffInjected) return;
targetContainer.dataset.remDiffInjected = '1';
targetContainer.style.display = 'block';
targetContainer.style.position = 'relative';
targetContainer.style.maxWidth = '600px';
targetContainer.style.margin = '-25px auto';
c1.style.width = '100%';
c1.style.height = 'auto';
c2.style.position = 'absolute';
c2.style.top = '0';
c2.style.left = '0';
c2.style.width = '100%';
c2.style.height = '100%';
c2.style.opacity = '0';
c2.style.pointerEvents = 'none';
const btn = document.createElement('button');
btn.className = 'rem-bp-exec-btn cs-button z-0 cursor-pointer group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-medium subpixel-antialiased transition-all tap-highlight-transparent transform-gpu data-[pressed=true]:scale-[0.97] opacity-100 outline-none focus-visible:outline-hidden ring-0 rounded-full px-4 h-9 min-w-9 text-sm bg-primary text-primary-foreground hover:bg-primary/90';
btn.style.marginTop = '2px';
btn.style.marginLeft = 'auto';
btn.style.marginRight = 'auto';
btn.style.display = 'flex';
btn.textContent = 'Поменять';
let showFirst = true;
btn.onclick = (e) => {
e.preventDefault();
showFirst = !showFirst;
c2.style.opacity = showFirst ? '0' : '1';
c2.style.pointerEvents = showFirst ? 'none' : 'auto';
};
const textContainer = targetContainer.previousElementSibling;
if (textContainer && textContainer.classList.contains('text-center')) {
textContainer.appendChild(btn);
} else {
targetContainer.parentNode.insertBefore(btn, targetContainer);
}
}
const p = new URLSearchParams(location.search);
if (p.get('fix_id')) {
const run = () => runFix(p.get('fix_id'), p.get('fix_name'));
if (document.readyState === 'loading') addEventListener('DOMContentLoaded', run); else run();
}
window.addEventListener('load', () => Manager.init());
const deckCountCache = new Map();
const deckCountPending = new Set();
async function fetchDeckOpenedCount(deckId) {
if (!Manager.userId) return 0;
if (deckCountCache.has(deckId)) return deckCountCache.get(deckId);
if (deckCountPending.has(deckId)) return -1;
deckCountPending.add(deckId);
try {
const data = await Manager.req(`${API_DOMAIN}/api/v2/inventory/decks/?is_opened=true&user_id=${Manager.userId}&deck_id=${deckId}&page=1`);
const count = data && data.count !== undefined ? data.count : 0;
deckCountCache.set(deckId, count);
return count;
} catch (e) {
deckCountCache.set(deckId, 0);
return 0;
} finally {
deckCountPending.delete(deckId);
}
}
function isDeckShopPage() {
return /\/customization\/feed\/deck/.test(location.pathname + location.search);
}
function isDeckInventoryPage() {
return /\/user\/\d+\/inventory/.test(location.pathname) && /type=decks/.test(location.search);
}
function isDeckInnerPage() {
return /\/deck\/\d+\/open/.test(location.pathname);
}
let lastDeckScanUrl = '';
async function scanDeckCards() {
if (!Manager.userId) return;
if (!isDeckShopPage() && !isDeckInventoryPage()) return;
const currentUrl = location.pathname + location.search;
if (lastDeckScanUrl !== currentUrl) {
lastDeckScanUrl = currentUrl;
document.querySelectorAll('.rem-deck-count-badge').forEach(b => b.remove());
document.querySelectorAll('[data-rem-deck-counted]').forEach(el => el.removeAttribute('data-rem-deck-counted'));
}
const deckLinks = document.querySelectorAll('a[href*="/deck/"][href*="/open"]:not([data-rem-deck-counted])');
for (const link of deckLinks) {
const m = link.getAttribute('href').match(/\/deck\/(\d+)\/open/);
if (!m) continue;
const deckId = m[1];
link.setAttribute('data-rem-deck-counted', '1');
const cardEl = link.querySelector('.cs-item-card, .relative') || link.firstElementChild;
if (!cardEl) continue;
if (window.getComputedStyle(cardEl).position === 'static') cardEl.style.position = 'relative';
if (cardEl.querySelector('.rem-deck-count-badge')) continue;
const badge = document.createElement('div');
badge.className = 'rem-deck-count-badge';
badge.textContent = '...';
cardEl.appendChild(badge);
fetchDeckOpenedCount(deckId).then(count => {
if (count < 0) return;
badge.textContent = count;
if (count > 0) badge.classList.add('has-count');
});
}
}
async function scanDeckInnerPage() {
if (!Manager.userId) return;
if (!isDeckInnerPage()) return;
if (document.querySelector('.rem-deck-inner-count')) return;
const m = location.pathname.match(/\/deck\/(\d+)\/open/);
if (!m) return;
const deckId = m[1];
const headings = document.querySelectorAll('h2, h3, h4, p, span, div');
let targetEl = null;
for (const el of headings) {
const txt = el.textContent.trim();
if (txt === 'Содержимое пака' && !el.querySelector('.rem-deck-inner-count')) {
targetEl = el;
break;
}
}
if (!targetEl) return;
const countEl = document.createElement('div');
countEl.className = 'rem-deck-inner-count';
countEl.innerHTML = '📦 Открыто: <strong>...</strong>';
targetEl.parentNode.insertBefore(countEl, targetEl);
const count = await fetchDeckOpenedCount(deckId);
countEl.innerHTML = `📦 Открыто: <strong>${count >= 0 ? count : '...'}</strong>`;
}
setInterval(() => {
checkNavigation();
scanVisual();
if (typeof injectTitleProgress === 'function') injectTitleProgress();
scanForDialog();
injectBattlepassExecuteButtons();
injectMemoryGameButton();
injectPuzzleGameButton();
injectDifferenceGameMod();
scanQuiz();
scanDeckCards();
scanDeckInnerPage();
if (cfg.onlineStatus) checkOnline();
if (location.pathname.startsWith('/user/')) { injectStats(); injectScanButton(); }
}, 400);
function scanQuiz() {
if (!location.pathname.includes('/user/battlepass/games/quiz')) return;
const qElem = document.querySelector('h2.cs-text');
if (!qElem) return;
const question = qElem.textContent.toLowerCase().replace(/\s+/g, ' ').trim();
const authorLink = document.querySelector('a[title="Автор"]');
if (!authorLink) return;
if (authorLink.dataset.remAnswerInjected && authorLink.dataset.remAnswerInjected !== question) {
const oldBadge = authorLink.parentNode.querySelector('.rem-quiz-answer');
if (oldBadge) oldBadge.textContent = '';
}
if (authorLink.dataset.remAnswerInjected === question) return;
const answer = quizDb[question] || quizDb[question.replace('?', '')];
if (answer) {
let badge = authorLink.parentNode.querySelector('.rem-quiz-answer');
if (!badge) {
badge = document.createElement('span');
badge.className = 'rem-quiz-answer';
badge.style.marginLeft = '12px';
badge.style.color = '#22c55e';
badge.style.fontWeight = 'bold';
badge.style.fontSize = '14px';
authorLink.parentNode.appendChild(badge);
}
badge.textContent = `✅ Ответ: ${answer}`;
}
authorLink.dataset.remAnswerInjected = question;
}
const quizDb = {
"какой тайтл не является исекаем?": "Тетрадь Смерти",
"в \"death note\" лайт ягами использует свою тетрадь с определённой целью. какую?": "Создать мир без преступности",
"в каком тайтле герой переносится в игровой мир, где становится правителем великой гробницы назарик?": "Повелитель",
"в какой манге действия происходят в постапокалиптическом мире с гигантскими стенами?": "Атака Титанов",
"какой из этих тайтлов основан на реальных мифологических концепциях и персонажах?": "Повесть о Конце Света",
"во сколько прибыл годжо сатору в сибуя?": "20:31",
"какой фонд поддерживал семью джостаров?": "Фонд Спидвагона",
"реально научиться по видео драться ? а ещё и удачно создать ютуб канал ? думаю эта манхва и аниме даст вам ответ.": "Борьба в прямом эфире",
"в какой манге используется так называемый нен?": "Хантер х Хантер",
"сколько процентов населения убил эрен йегер?": "80",
"как погиб римуру темпест в своей прошлой жизни": "Зарезан грабителем",
"какая сила гиасса у лелуша в манге \"код гиасс\"?": "Способность подчинять людей",
"какой вид кагуне получил канеки после операции?": "Ринкаку",
"как зовут штормового дракона, в манге \"о моем перерождении в слизь\"?": "Вельдора",
"как звали главного героя манхвы \"поднятие уровня в одиночку\"": "Сон Джин У",
"что из перечисленного не является сэйнэном?": "Выбери меня",
"чего добился под конец своей жизни корол пиратов, голд роджер?": "Голд Роджер, Король Пиратов, добился всего, что может предложить этот мир",
"раз во сколько лет происходит солнечное затмение в берсерке?": "216",
"в каком году происходят события атаки титанов?": "845",
"что такое remanga?": "Платформа для чтения манги, манхвы, маньхуа, комиксов",
"что такое \"сёнэн\"?": "Жанр аниме для мальчиков.",
"сколько стоит создание гильдии?": "2940",
"мало кто помнит, но во втором repass изменили названия получаемых бейджей. какой первый бейдж получали пользователи?": "Читатель FFF ранга",
"уникальный пак карт, не просто новый, а особенный, созданный в коллабе с крутым художником!": "Кацузавр",
"в наше время с чем только не поднимают уровень.... а ведь правда, с чем из перечисленного ещё не поднимали уровень прямо в названии?": "С фамильярами",
"что за манга \"дневник...\"?": "Будущего",
"на сколько молний можно обменять 1 тикет?": "75",
"сколько молний получает пользователь за вход на сайт 1 раз за день? (без бонусов гульдий и премиум аккаунта)": "20",
"сколько лайков нужно поставить для получения ачивки \"сердце читателя\" xr ранга?": "20 000",
"назовите какого класса не существует в «ассоциации героев» из манги \"ванпачмен\"?": "F-класс",
"из которого аниме эта фраза \"выньте жесткий диск из моего домашнего компьютера… поместите его в ванну и убедитесь, что он полностью стерт\"": "О моём перерождении в слизь",
"какое самое кассовое аниме в истории?": "Истребитель демонов: Поезд „Бесконечный“",
"кто из перечисленных не состоял в акацуки?": "Чиё",
"первое аниме, показанное в ссср": "Летающий корабль-призрак",
"кто из перечисленных не относится к муравьям-химерам из манги \"охотник х охотник\"?": "Хисока",
"какой стенд у дио брандо?": "The world",
"кто является главным антагонистом первой арки \"re:zero?\"": "Эльза",
"кто является первым убийцей драконов в \"хвосте фей\"": "Акнология",
"сколько существует школьных тайн в японии?": "7",
"какой цвет волос часто ассоциируется с цундере-персонажами?": "Розовый или красный",
"в каком аниме главные герои участвуют в смертельной игре с телефонами?": "Дневник будущего",
"кто из этих персонажей использует тетрадь смерти?": "Лайт Ягами",
"какой хвостатый зверь был первым, кто подружился с наруто?": "Сон Гоку (Четырёххвостый)",
"в каком аниме главный герой превращается в титана?": "Атака титанов",
"что будет, если написать имя человека в тетради смерти без указания причины смерти?": "Он умрёт от сердечного приступа",
"сколько отжимыний каждый день делал сайтама?": "100",
"что будет, если в «re:zero» главный герой погибает?": "Он возвращается в предыдущую точку",
"как зовут старшую сестру тандзиро из \"истребителя демонов\"?": "Незуко",
"как называется магический предмет, с помощью которого лайт ягами может убивать людей?": "Тетрадь смерти",
"чем отличается манга от манхвы?": "Направлением чтения",
"сколько типов тайтлов можно выбрать в фильтре каталога?": "7",
"сколько карт в паке читай город?": "5 карт",
"кто первый создал s ранговую карту?": "muzeek0",
"какой максимальный ранг может быть у ачивки?": "XR",
"сколько разделов в магазине?": "6",
"сколько карт было в паке на 7-ое день рождение?": "10",
"за какую активность нельзя получить молнии на сайте?": "Комментарии при обмене",
"сколько рангов карт существует на реманге?": "8",
"как называется карточка что может эволюционировать и сколько у неё этапов эволюции?": "Загадочный горшок. 8 этапов эволюции.",
"как в начале манхвы «поднятие уровня в одиночку» называли джин ву?": "Слабейшее оружие человечества",
"кто оставил старку шрам на лице": "Айзен",
"из за чего хината захотел играть в волейбол? из \"волейбол!!\"": "Увидел по телевизору",
"\"всеведующий читатель\" манхва о том как вокруг главного героя начинают происходить события книги, которую он читал на протяжении пол своей жизни, её название \"три способа выжить в разрушенном мире\". сколько в ней глав?": "3149",
"сколько раз убил себя ким кон чжа из охотник sss-уровня что бы вернутся на 4050 дней?": "4080",
"какой был игровой ник у главного персонажа \"выбери меня!\"?": "Локи",
"кто является главным героем в \"моб психо 100\"?": "Сигэо Кагэяма",
"как называется гримуар асты в \"чёрный клевер\"?": "Пятилистный Гримуар",
"как называется организация супергероев у сайтамы в \"one punch man\"?": "Ассоциация Героев",
"что в начале истории случилось с главным героем произведения \"убийца педро\"?": "Его предали,после чего на грани смерти он заново обрёл молодость",
"как зовут главного героя в манхве \"башня бога\"?": "Двадцать Пятый Бэм",
"как зовут демона-бензопилу в \"человек-бензопила\", с которым заключил контракт денджи?": "Почита",
"какая конвертация монет к молниям?": "1 к 5",
"кем был артур из \"начало после конца\" в своей прошлой жизни?": "Королём",
"от кого можно получить наилучший любовный совет?": "Герцог Ада",
"каким номером в своём отряде был главный героя произведения \"прирождённый наёмник\"?": "001",
"какой организации начинает служить денджи после своей смерти и перерождения в \"человек-бензопила\"?": "Специальный отряд безопасности",
"в какой отряд рыцарей-чародеев присоединился аста из произведения \"чёрный клевер\"?": "Черный Бык",
"какой навык позволил ким докче пережить свой первый контакт с \"точкой разрушения\"?": "Читательская стойкость",
"где находится \"клеймо жертвы\" гатса из манги \"берсерк\"?": "Правая сторона шеи",
"в какой манге/аниме-исекае главный герой, увлеченный идеей стать “скрытым владыкой тьмы”, тренируется в лесу и внезапно погибает, что приводит к его перерождению в другом мире?": "Восхождение в Тени",
"какое имя было у артура лейвина из комикса \"начало после конца\" в прошлой жизни?": "Грей",
"сколько листков клевера было на гримуаре главного героя из аниме \"чёрный клевер\"?": "5",
"сколько глав в новелле \"боевой пик\"?": "Более шести тысяч",
"что из этого является произведением про боевые исскуства?": "Безупречный отец",
"кае называется любительское произведение, чаще всего написанная с использованием персонажей известного художественного произведения, но обладающая самостоятельным сюжетом?": "Додзинси",
"карточку какого гг дают при создании аккаунта на реманге?": "Не Ли",
"как зовут истинную форму алукарда, освобожденную после снятия ограничителей?": "Владыка Ночи",
"как звали первого джостара из джо джо?": "Джонатан",
"сколько лет сайту \"реманга\" на момент 2025г?": "7",
"сколько горячих новинок вмещает слайдер на главной странице сайта?": "20",
"на какого паблишера нужно подписаться, чтобы следить за актуальными новостями сайта?": "Реманга вещает!",
"сколько нужно поставить лайков на сайте \"реманга\", чтобы получить ачивку максимального ранга \"сердце читателя xr ранга\"?": "20.000",
"с какого количества гильдий, в которых ты состоишь, можно получить бонус?": "Одна",
"какой бонус можно получить за подписку на \"реманга\"?": "Бонус 2.000 молний",
"небесный демон в старшей школе - это...": "Манхва",
"регрессия - это...": "Сюжет, где главный герой возвращается в прошлое с сохранением воспоминаний и опыта из будущего.",
"куда писать, если у тебя баг?": "В форму обратной связи на сайте и модерацию сайта через официальный паблик вк.",
"можно ли исключить теги на форуме, чтобы не видеть неинтересные посты?": "Да",
"как маомао попала во дворец из манги \"монолог фармацевта\"?": "Её похитили и продали во дворец",
"в каком произведении главный герой, будучи обычным студентом, случайно обнаруживает, что его сосед по комнате является древним демоном? при этом демон вынужден подчиняться приказам главного героя из-за древнего магического контракта, который они заключили по ошибке.": "Очень приятно, Бог",
"кем хочет стать руби хошино из аниме и манги \"звёздное дитя\"?": "Айдолом",
"главный герой какой известной манхы зовут артур лейвин?": "Начало после конца",
"какой вид произведений рисуют в корее?": "Манхва",
"какие из произведений были выпущены в один год?": "Хоримия и Сквозь бальный зал",
"самый популярный законченный тайтл?": "Милый дом",
"что больше всего любит ллойд из \"система всемогущего дизайнера\"": "Деньги",
"имя главной героини \"провожающей в последний путь\"?": "Фрирен",
"какая особенность у главной героини юки из манги \"любовь с кончиков пальцев\"?": "Она глухая",
"пиццу какой сети постоянно ели персонажи в \"коде гиасе\"?": "Pizza Hut",
"как зовут моба из \"моб психо 100\"": "Кагэяма Шигео",
"в каком разделе можно найти самые популярные произведения?": "В топе",
"какой единственный режиссер аниме, который удостоился премии оскар?": "Хаяо Миядаки",
"в каком формате нельзя читать мангу на сайте?": "В формате PDF",
"что такое \"таймскип\" в манге?": "Прыжок во времени в сюжете",
"какое имя получила элейна из аниме \"путешествие элейны\", когда стала ведьмой?": "Пепельная ведьма",
"что такое \"фанбук\" в манге?": "Книга с дополнительными материалами от автора",
"как зовут синигами, которому принадлежала тетрадь смерти, найденная лайтом ягами?": "Рюк",
"главный герой аниме \"van pis\"": "Луффи",
"какая манхва является предысторией милого дома?": "Мальчик с ружьём",
"кто был главным героем в лукизме?": "Даниэль Пак",
"сколько жён было у тенгена из манги \"клинок, рассекающий демона\"?": "3",
"чем мэш защищается от магии?": "Физической силой",
"где обычно культиваторы собирают и хранят свою ци?": "В Даньтяне",
"укажите лишний тайтл": "Бакуман",
"как называлась главная подпольная, преступная организация из манги «великий из бродячих псов»?": "Портовая мафия",
"в каком аниме героиня получает магическую силу от геометрической фигуры, связанной со спутником земли?": "Сейлор Мун",
"как называется меч гатса?": "Драконоборец",
"какой фрукт дьявола съел луффи?": "Гому-Гому но Ми",
"кто главный герой манхвы «башня бога»?": "Баам",
"как зовут дракона, связанного с главным героем артуром лейвином в манхве «начало после конца»?": "Сильви",
"как называется меч танджиро в \"клинке, рассекающем демонов\"?": "Ничирин",
"какой из этих специалистов не реинкарнировал?": "Никто, все реинкарнировали",
"сколько хвостов было у демона-лисы по имени курама?": "9",
"как итодори юди избавляется от пальцев сукуна?": "Съедает их",
"в манге \"паразит\" как зовут паразита, захватившего руку главного героя синъити идзуми?": "Миги",
"какое настоящее имя персонажа \"l\" из манги тетрадь смерти?": "Эл Лоулайт",
"сколько всего эмодзи-паков в магазине реманги?": "12",
"сколько нужно прочитать глав чтоб получить ачивку \"погружённый в чтение xr ранга\"?": "40.000",
"как называется текстовая манга?": "Ранобэ",
"первая карта а ранга": "Хитори Гото \"Одинокий Рокер\"",
"когда день рождения remanga?": "25 мая",
"сколько существует базовых ачивок на сайте?": "9",
"месяц, когда очень хочется солгать": "Апрель",
"аниме это что?": "Японский мультик",
"когда можно было получить бейдж «адепт темной стороны» и «адепт светлой стороны»?": "Ивент «недрочабрь» в 2022г.",
"кто читает этот вопрос?": "Лучший человек на свете",
"имя главного героя из тайтла \"элисед \"": "Со Джи У",
"кем хотел стать наруто узумаки в аниме \"наруто\"?": "Хокаге",
"что означает термин \"сэйнэн\"?": "Манга для взрослой аудитории",
"лучший пту для тех кто хочет стать героем в манге \"моя геройская академии\"?": "Академия Шинро",
"что такое \"фансервис\" в манге?": "Элементы для привлечения внимания аудитории",
"какая раса первая подчинилась римуру, в манге \"о моём перерождении в слизь\"?": "Гоблины",
"сколько действующих капитанов в готей 13 в аниме \"блич\"?": "13",
"какой тайтл не был выпущен recomics.org?": "Город гоблинов",
"что обозначает термин \"сабу\"?": "Наставник",
"какой жанр чаще всего включает в себя перерождение главного героя?": "Исекай",
"какая самая первая s ранговая карта?": "Токийский гуль",
"какой персонаж не вошёл в пак «читай город»?": "Марк Твен",
"сколько существует вкладок в магазине?": "6",
"какой самый популярный тайтл по количеству оценок?": "Поднятие уровня в одиночку",
"сколько дают тикетов бонусом при пополнении 1к рублей?": "7",
"когда нужно начинать пропаганду тюленей?": "Пока админы спят",
"сколько лет реманге?": "7",
"сколько даст 1 тикет молнии?": "75",
"как называется категория аниме, где гг переносится в другой мир?": "Исекай",
"Сколько существует фильтров по поиску тайтла, не считая расширенный фильтр и исключения?": "5",
"Как называется организация супергероев у Сайтамы в One Punch Man?": "Ассоциация Героев",
"Кто является главным антагонистом первой арки \"Re:Zero\"\"?": "Эльза",
"Назовите какого класса НЕ существует в \"Ассоциации героев\" из манги Ванпачмен?": "F-класс",
"Как в начале манхвы \"Поднятие уровня в одиночку\" называли Джин Ву?": "Слабейшее оружие человечества",
"В манхве \"Лукизм\" какую особенность имеет главный герой Пак Хён Сок?": "У него два тела — одно красивое, другое обычное"
};
})();