удобный мультитул для AnimeSSS
// ==UserScript==
// @name ASSs MultiTools
// @namespace https://animesss.com/
// @version 1.3
// @description удобный мультитул для AnimeSSS
// @author SoulUA
// @license MIT
// @match *://animesss.tv/*
// @match *://animesss.com/*
// @run-at document-start
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function () {
'use strict';
const SCRIPT_NAME = 'ASS TM Combo';
const SETTING_PREFIX = 'ass_tm_combo_';
const DEFAULT_PROFILE_BUTTONS = [
{ id: 'need', enabled: true, text: '', icon: 'fal fa-search', url: '/user/cards/need/?name={USERNAME}' },
{ id: 'in-list', enabled: true, text: '', icon: 'fal fa-heart', url: '/user/cards/?name={USERNAME}&in_list=1' },
{ id: 'unlocked', enabled: true, text: '', icon: 'fal fa-unlock', url: '/user/cards/?name={USERNAME}&locked=0' },
{ id: 'club', enabled: true, text: '', icon: 'fal fa-users', url: '/clubs/{CLUB_ID}/' },
...['s_plus', 's', 'a_plus', 'a', 'b_plus', 'b', 'c_plus', 'c', 'd_plus', 'd', 'e_plus', 'e', 'ass'].map((rank) => ({
id: `rank-${rank}`,
enabled: true,
text: rank.replace('_plus', '+').toUpperCase(),
icon: '',
url: `/user/cards/?name={USERNAME}&locked=0&rank=${rank}`,
})),
];
const DEFAULT_QN_LINKS = [
{ id: 'qn-1', title: 'Лента карт', url: '/cards' },
{ id: 'qn-2', title: 'Паки карт', url: '/cards/pack/' },
{ id: 'qn-3', title: 'Трейды', url: '/trades/offers/' }
];
const FONT_OPTIONS = [
{ value: 'system-ui, sans-serif', label: 'Системный (По умолчанию)' },
{ value: 'Georgia, serif', label: 'Georgia (Элегантный)' },
{ value: '"Palatino Linotype", "Book Antiqua", serif', label: 'Palatino (Книжный)' },
{ value: '"Trebuchet MS", sans-serif', label: 'Trebuchet MS (Компактный)' },
{ value: 'Impact, sans-serif', label: 'Impact (Массивный)' },
{ value: '"Arial Black", sans-serif', label: 'Arial Black (Жирный)' },
{ value: '"Comic Sans MS", cursive', label: 'Comic Sans (Неформальный)' },
{ value: '"Courier New", monospace', label: 'Courier New (Печатная машинка)' },
{ value: '"Lucida Console", monospace', label: 'Lucida Console (Терминал)' }
];
function cloneValue(value) {
try { return JSON.parse(JSON.stringify(value)); } catch (_) { return value; }
}
const DEFAULTS = {
profileButtons: true,
cardNeedButtons: true,
modalStarButton: true,
takeCinemaStone: false,
takeSnowStone: false,
takeHeavenlyStone: false,
stoneBaseDelayMs: 900,
stoneGrowthDelayMs: 250,
quickNavEnabled: true,
qnBgColor: '#121212',
qnBgOpacity: 0.0,
qnBlur: 0,
qnBtnBgColor: '#212121',
qnBtnTextColor: '#e0e0e0',
qnFontFamily: 'system-ui, sans-serif',
qnBtnFontSize: 13,
qnBtnPadY: 10,
qnBtnPadX: 16,
profBtnBgColor: '#2b2b2b',
profBtnHoverBgColor: '#9e294f',
profBtnTextColor: '#e0e0e0',
profFontFamily: 'system-ui, sans-serif',
profBtnFontSize: 12,
profBtnPadX: 8,
profBtnHeight: 28,
profileButtonsConfig: cloneValue(DEFAULT_PROFILE_BUTTONS),
qnLinks: cloneValue(DEFAULT_QN_LINKS),
};
const state = {};
const clickedCinemaCodes = new Set();
let qnResizeObserver = null;
let qnIsScrollListenerAttached = false;
let stoneClickQueue = 0;
// ---------------------------------------------------------------------------
// Core Utilities & Toast UI
// ---------------------------------------------------------------------------
function showToast(msg) {
let toast = document.getElementById('ass-tm-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'ass-tm-toast';
toast.style.cssText = 'position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background:rgba(0,0,0,0.85); color:#fff; padding:10px 20px; border-radius:12px; z-index:9999999; font-size:13px; font-family:sans-serif; pointer-events:none; opacity:0; transition:opacity 0.3s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.3); font-weight: 600; text-align: center; white-space: nowrap;';
document.body.appendChild(toast);
}
toast.textContent = msg;
toast.style.opacity = '1';
clearTimeout(toast.timer);
toast.timer = setTimeout(() => { toast.style.opacity = '0'; }, 3000);
}
function gmGet(key, fallback) {
try { if (typeof GM_getValue === 'function') return GM_getValue(SETTING_PREFIX + key, fallback); } catch (_) { }
try { const raw = localStorage.getItem(SETTING_PREFIX + key); return raw == null ? fallback : JSON.parse(raw); } catch (_) { return fallback; }
}
function gmSet(key, value) {
state[key] = value;
try { if (typeof GM_setValue === 'function') { GM_setValue(SETTING_PREFIX + key, value); return; } } catch (_) { }
try { localStorage.setItem(SETTING_PREFIX + key, JSON.stringify(value)); } catch (_) { }
}
function loadSettings() {
for (const [key, value] of Object.entries(DEFAULTS)) {
state[key] = gmGet(key, cloneValue(value));
if ((key === 'profileButtonsConfig' || key === 'qnLinks') && !Array.isArray(state[key])) {
state[key] = cloneValue(value);
}
}
}
function onBodyReady(fn) {
if (document.body) return fn();
const timer = setInterval(() => { if (!document.body) return; clearInterval(timer); fn(); }, 50);
}
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
function injectStyle() {
const css = `
.user-card-buttons { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-left: 0; width: 100%; justify-content: flex-start; }
.user-card-buttons a {
display: inline-flex; align-items: center; justify-content: center; min-width: 28px;
height: var(--prof-btn-h, 28px); padding: 0 var(--prof-btn-padx, 8px);
border-radius: 8px; background: var(--prof-btn-bg, rgba(255,255,255,.08));
color: var(--prof-btn-text, inherit); font-family: var(--prof-btn-font, system-ui, sans-serif);
text-decoration: none; font-size: var(--prof-btn-fs, 12px); font-weight: 600; line-height: 1; transition: background-color 0.2s ease;
}
.user-card-buttons a:hover { background: var(--prof-btn-hover, rgba(158,41,79,.85)); }
.as-ext-star-meta-item { flex: 0 0 auto; }
#ass-tm-settings-fab { display: inline-flex; align-items: center; justify-content: center; min-width: 38px; height: 38px; border: 0; border-radius: 12px; background: rgba(255,255,255,.10); color: #fff; font: 18px/1 Arial, sans-serif; cursor: pointer; text-decoration: none; box-sizing: border-box; transition: background 0.2s; }
#ass-tm-settings-fab:hover { background: rgba(158,41,79,.92); }
#ass-tm-settings-fab.ass-tm-settings-fallback { position: fixed; z-index: 999998; right: 12px; top: 88px; box-shadow: 0 8px 24px rgba(0,0,0,.28); }
.ass-tm-settings-backdrop { position: fixed; inset: 0; z-index: 999999; display: none; align-items: center; justify-content: center; background: rgba(0,0,0,.55); padding: 14px; box-sizing: border-box; backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); }
.ass-tm-settings-backdrop.ass-tm-settings-open { display: flex; }
.ass-tm-settings-panel { width: min(760px, 100%); max-height: min(86vh, 760px); overflow: auto; border-radius: 14px; background: #171719; color: #f4f4f4; box-shadow: 0 18px 56px rgba(0,0,0,.45); font: 13px/1.35 Arial, sans-serif; }
.ass-tm-settings-head { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 14px; background: #202024; border-bottom: 1px solid rgba(255,255,255,.08); }
.ass-tm-settings-title { font-weight: 700; font-size: 15px; }
.ass-tm-settings-close { border: 0; border-radius: 8px; background: rgba(255,255,255,.08); color: #fff; min-width: 34px; height: 32px; cursor: pointer; transition: background 0.2s; }
.ass-tm-settings-close:hover { background: rgba(255,255,255,.15); }
.ass-tm-settings-body { padding: 12px 14px 16px; }
.ass-tm-settings-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.ass-tm-settings-section { border: 1px solid rgba(255,255,255,.08); border-radius: 12px; padding: 10px; background: rgba(255,255,255,.035); }
.ass-tm-settings-section h3 { margin: 0 0 8px; font-size: 13px; color: #fff; }
.ass-tm-setting-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; min-height: 34px; border-top: 1px solid rgba(255,255,255,.06); padding: 7px 0; }
.ass-tm-setting-row:first-of-type { border-top: 0; }
.ass-tm-setting-label { flex: 1 1 auto; min-width: 50%; }
.ass-tm-setting-row input[type="number"], .ass-tm-setting-row input[type="text"], .ass-tm-editor-row input[type="text"] { border: 1px solid rgba(255,255,255,.14); border-radius: 8px; background: #0f0f11; color: #fff; padding: 6px 8px; box-sizing: border-box; }
.ass-tm-setting-row select {
border: 1px solid rgba(255,255,255,.14);
border-radius: 8px;
background-color: #0f0f11 !important;
background-image: none !important;
color: #fff;
padding: 6px 36px 6px 8px;
box-sizing: border-box;
width: 140px;
outline: none;
font-size: 12px;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.ass-tm-setting-row {
position: relative;
}
.ass-tm-setting-row select {
background-repeat: no-repeat !important;
background-size: 0 0 !important;
}
.ass-tm-setting-row input[type="number"] { width: 104px; }
.ass-tm-setting-row input[type="color"] { background: none; border: none; width: 32px; height: 32px; cursor: pointer; padding: 0; border-radius: 6px; }
.ass-tm-setting-row input[type="checkbox"], .ass-tm-editor-row input[type="checkbox"] { width: 18px; height: 18px; accent-color: #9e294f; cursor: pointer; margin: 0; }
.ass-tm-editor { margin-top: 12px; border: 1px solid rgba(255,255,255,.08); border-radius: 12px; padding: 10px; background: rgba(255,255,255,.035); }
.ass-tm-editor h3 { margin: 0 0 8px; font-size: 13px; }
.ass-tm-editor-list { display: flex; flex-direction: column; gap: 8px; }
/* ========================================================= */
/* --- ПУЛЕНЕПРОБИВАЕМЫЙ CSS (из скриншотов) --- */
.ass-tm-editor-row {
display: grid;
gap: 8px;
padding: 10px;
border-radius: 8px;
background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.05);
align-items: center;
grid-template-columns: 24px minmax(0, 1fr) minmax(0, 1fr) 34px;
grid-template-rows: auto auto;
}
.ass-tm-editor-row > [data-f="en"] {
grid-column: 1;
grid-row: 1;
justify-self: center;
margin: 0;
}
.ass-tm-editor-row > [data-f="txt"] {
grid-column: 2;
grid-row: 1;
width: 100%;
min-width: 0;
margin: 0;
}
.ass-tm-editor-row > [data-f="icn"] {
grid-column: 3;
grid-row: 1;
width: 100%;
min-width: 0;
margin: 0;
}
.ass-tm-editor-row > .ass-tm-editor-remove {
grid-column: 4;
grid-row: 1;
width: 100%;
height: 34px;
margin: 0;
border: 0;
border-radius: 8px;
background: rgba(255,255,255,.08);
color: #fff;
cursor: pointer;
transition: background 0.2s;
}
.ass-tm-editor-row > .ass-tm-editor-remove:hover {
background: rgba(190,40,40,.9);
}
.ass-tm-editor-row > [data-f="url"] {
grid-column: 1 / -1;
grid-row: 2;
width: 100%;
min-width: 0;
margin: 0;
}
/* Стили для ссылок Quick Nav (у них нет галочки и иконки) */
.ass-tm-editor-row.qn-link {
grid-template-columns: minmax(0, 1fr) 34px;
}
.ass-tm-editor-row.qn-link > [data-f="ttl"] {
grid-column: 1;
grid-row: 1;
width: 100%;
min-width: 0;
margin: 0;
}
.ass-tm-editor-row.qn-link > .ass-tm-editor-remove {
grid-column: 2;
grid-row: 1;
}
@media (min-width: 681px) {
.ass-tm-editor-row {
grid-template-columns: 24px 1fr 1fr 2fr 34px;
grid-template-rows: auto;
padding: 6px;
background: rgba(255,255,255,.02);
border: none;
}
.ass-tm-editor-row > [data-f="en"] {
grid-column: 1;
grid-row: 1;
}
.ass-tm-editor-row > [data-f="txt"] {
grid-column: 2;
grid-row: 1;
}
.ass-tm-editor-row > [data-f="icn"] {
grid-column: 3;
grid-row: 1;
}
.ass-tm-editor-row > .ass-tm-editor-remove {
grid-column: 5;
grid-row: 1;
}
.ass-tm-editor-row > [data-f="url"] {
grid-column: 4;
grid-row: 1;
}
.ass-tm-editor-row.qn-link {
grid-template-columns: 1fr 2fr 34px;
}
.ass-tm-editor-row.qn-link > [data-f="ttl"] {
grid-column: 1;
grid-row: 1;
}
.ass-tm-editor-row.qn-link > [data-f="url"] {
grid-column: 2;
grid-row: 1;
}
.ass-tm-editor-row.qn-link > .ass-tm-editor-remove {
grid-column: 3;
grid-row: 1;
}
}
/* Mobile overrides for standard settings */
@media (max-width: 680px) {
.ass-tm-settings-grid { grid-template-columns: 1fr; }
.ass-tm-settings-backdrop { padding: 8px; }
.ass-tm-settings-panel { max-height: 95vh; }
.ass-tm-settings-body { padding: 12px 10px; }
.ass-tm-setting-row { flex-direction: column; align-items: flex-start; gap: 6px; }
.ass-tm-setting-row input[type="number"], .ass-tm-setting-row input[type="text"], .ass-tm-setting-row select {
flex: 0 0 100%; max-width: none; width: 100%; margin-top: 4px;
}
}
/* ========================================================= */
.ass-tm-editor-actions, .ass-tm-settings-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.ass-tm-settings-actions { margin-top: 12px; }
.ass-tm-editor-actions button, .ass-tm-settings-actions button { border: 0; border-radius: 9px; background: rgba(255,255,255,.1); color: #fff; padding: 8px 10px; cursor: pointer; font-weight: 600; transition: background 0.2s;}
.ass-tm-editor-actions button:hover, .ass-tm-settings-actions button:hover { background: rgba(158,41,79,.85); }
.ass-tm-save-status { margin-top: 10px; min-height: 16px; color: rgba(255,255,255,.62); font-size: 11px; }
/* Quick Nav UI Styles */
#custom-quick-nav {
display: flex; gap: 8px; padding: 12px 16px; overflow-x: auto; white-space: nowrap; scrollbar-width: none;
-ms-overflow-style: none; border-bottom: none; box-shadow: none; position: fixed; box-sizing: border-box;
z-index: 998; justify-content: safe center; pointer-events: none; transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
}
#custom-quick-nav > * { pointer-events: auto; }
#custom-quick-nav::-webkit-scrollbar { display: none; }
#aqn-spacer { width: 100%; display: block; flex-shrink: 0; }
.custom-nav-btn {
background-color: var(--btn-bg, #212121); color: var(--btn-text, #e0e0e0); font-family: var(--btn-font, system-ui, sans-serif);
font-size: var(--btn-fs, 13px); padding: var(--btn-pad, 10px 16px); text-decoration: none; border-radius: 6px;
font-weight: 600; letter-spacing: 0.5px; transition: opacity 0.2s ease; flex-shrink: 0; border: none; cursor: pointer;
display: flex; align-items: center; justify-content: center; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.5);
}
.custom-nav-btn:hover, .custom-nav-btn:active { opacity: 0.8; }
`;
const style = document.createElement('style');
style.id = 'ass-tm-opt-combo-style';
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
}
// ---------------------------------------------------------------------------
// Profile & Quick Nav Visual Styling Engine
// ---------------------------------------------------------------------------
function hexToRgb(hex) {
let c;
if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
c = hex.substring(1).split('');
if (c.length === 3) c = [c[0], c[0], c[1], c[1], c[2], c[2]];
c = '0x' + c.join('');
return [(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',');
}
return '18,18,18';
}
function applyQnVisualSettings() {
const nav = document.getElementById('custom-quick-nav');
if (!nav) return;
const rgb = hexToRgb(state.qnBgColor || '#121212');
nav.style.backgroundColor = `rgba(${rgb}, ${state.qnBgOpacity})`;
nav.style.backdropFilter = `blur(${state.qnBlur}px)`;
nav.style.webkitBackdropFilter = `blur(${state.qnBlur}px)`;
nav.style.setProperty('--btn-bg', state.qnBtnBgColor || '#212121');
nav.style.setProperty('--btn-text', state.qnBtnTextColor || '#e0e0e0');
nav.style.setProperty('--btn-font', state.qnFontFamily || 'system-ui, sans-serif');
nav.style.setProperty('--btn-fs', `${state.qnBtnFontSize || 13}px`);
nav.style.setProperty('--btn-pad', `${state.qnBtnPadY || 10}px ${state.qnBtnPadX || 16}px`);
}
function applyProfVisualSettings() {
const root = document.documentElement;
root.style.setProperty('--prof-btn-bg', state.profBtnBgColor || 'rgba(255,255,255,.08)');
root.style.setProperty('--prof-btn-hover', state.profBtnHoverBgColor || 'rgba(158,41,79,.85)');
root.style.setProperty('--prof-btn-text', state.profBtnTextColor || 'inherit');
root.style.setProperty('--prof-btn-font', state.profFontFamily || 'system-ui, sans-serif');
root.style.setProperty('--prof-btn-fs', `${state.profBtnFontSize || 12}px`);
root.style.setProperty('--prof-btn-padx', `${state.profBtnPadX || 8}px`);
root.style.setProperty('--prof-btn-h', `${state.profBtnHeight || 28}px`);
}
function renderQnButtons() {
const nav = document.getElementById('custom-quick-nav');
if (!nav) return;
nav.innerHTML = '';
const fragment = document.createDocumentFragment();
(state.qnLinks || []).forEach(link => {
if (!link.url || !link.title) return;
const btn = document.createElement('a');
btn.href = link.url;
btn.textContent = link.title;
btn.className = 'custom-nav-btn';
fragment.appendChild(btn);
});
nav.appendChild(fragment);
}
function setupQnPositioning(targetElement, navContainer, spacer) {
if (qnResizeObserver) qnResizeObserver.disconnect();
const updateStickyPosition = () => {
if (!document.body.contains(targetElement) || !document.body.contains(navContainer)) return;
const rect = targetElement.getBoundingClientRect();
navContainer.style.top = `${rect.bottom}px`;
navContainer.style.left = `${rect.left}px`;
navContainer.style.width = `${rect.width}px`;
if (spacer) spacer.style.height = `${navContainer.offsetHeight}px`;
};
updateStickyPosition();
if (window.ResizeObserver) {
qnResizeObserver = new ResizeObserver(() => requestAnimationFrame(updateStickyPosition));
qnResizeObserver.observe(targetElement);
qnResizeObserver.observe(navContainer);
}
if (!qnIsScrollListenerAttached) {
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) { window.requestAnimationFrame(() => { updateStickyPosition(); ticking = false; }); ticking = true; }
}, { passive: true });
qnIsScrollListenerAttached = true;
}
}
function injectQuickNav() {
const existingNav = document.getElementById('custom-quick-nav');
const existingSpacer = document.getElementById('aqn-spacer');
if (!state.quickNavEnabled) {
if (existingNav) existingNav.remove();
if (existingSpacer) existingSpacer.remove();
if (qnResizeObserver) qnResizeObserver.disconnect();
return;
}
if (existingNav) return;
const targetElement = document.querySelector('header');
if (!targetElement) return;
const spacer = document.createElement('div');
spacer.id = 'aqn-spacer';
const navContainer = document.createElement('div');
navContainer.id = 'custom-quick-nav';
targetElement.insertAdjacentElement('afterend', spacer);
targetElement.insertAdjacentElement('afterend', navContainer);
renderQnButtons();
applyQnVisualSettings();
setupQnPositioning(targetElement, navContainer, spacer);
}
// ---------------------------------------------------------------------------
// Settings Logic & UI Schema
// ---------------------------------------------------------------------------
const SETTINGS_SCHEMA = [
{
title: 'Панель быстрого доступа',
items: [
{ key: 'quickNavEnabled', type: 'bool', label: 'Включить панель' },
{ key: 'qnBgColor', type: 'color', label: 'Цвет подложки' },
{ key: 'qnBgOpacity', type: 'number', step: '0.05', label: 'Непрозрачность (0-1)', min: 0, max: 1 },
{ key: 'qnBlur', type: 'number', label: 'Размытие фона (px)', min: 0, max: 20 },
{ key: 'qnBtnBgColor', type: 'color', label: 'Фон кнопок' },
{ key: 'qnBtnTextColor', type: 'color', label: 'Текст кнопок' },
{ key: 'qnFontFamily', type: 'select', label: 'Шрифт кнопок', options: FONT_OPTIONS },
{ key: 'qnBtnFontSize', type: 'number', label: 'Размер шрифта', min: 10, max: 24 },
{ key: 'qnBtnPadY', type: 'number', label: 'Отступ (вертикаль)', min: 4, max: 24 },
{ key: 'qnBtnPadX', type: 'number', label: 'Отступ (горизонталь)', min: 4, max: 32 },
],
},
{
title: 'Кнопки профиля / рангов (Внешний вид)',
items: [
{ key: 'profBtnBgColor', type: 'color', label: 'Фон кнопок' },
{ key: 'profBtnHoverBgColor', type: 'color', label: 'Фон при наведении' },
{ key: 'profBtnTextColor', type: 'color', label: 'Текст кнопок' },
{ key: 'profFontFamily', type: 'select', label: 'Шрифт кнопок', options: FONT_OPTIONS },
{ key: 'profBtnFontSize', type: 'number', label: 'Размер шрифта', min: 8, max: 24 },
{ key: 'profBtnPadX', type: 'number', label: 'Отступ (горизонталь)', min: 0, max: 32 },
{ key: 'profBtnHeight', type: 'number', label: 'Высота кнопки', min: 16, max: 48 },
],
},
{
title: 'Прочее',
items: [
{ key: 'profileButtons', type: 'bool', label: 'Включить кнопки в профиле' },
{ key: 'cardNeedButtons', type: 'bool', label: 'Кнопки need на карточках' },
{ key: 'modalStarButton', type: 'bool', label: 'Кнопка звезды в модалке' },
],
},
{
title: 'Камни (Автосбор)',
items: [
{ key: 'takeCinemaStone', type: 'bool', label: 'Автозабор cinema stone' },
{ key: 'takeSnowStone', type: 'bool', label: 'Автозабор snow stone' },
{ key: 'takeHeavenlyStone', type: 'bool', label: 'Автозабор heavenly stone' },
{ key: 'stoneBaseDelayMs', type: 'number', label: 'Базовая задержка клика, ms', min: 10, max: 5000 },
{ key: 'stoneGrowthDelayMs', type: 'number', label: 'Рост задержки, ms', min: 10, max: 3000 },
],
}
];
function getSettingDef(key) {
for (const section of SETTINGS_SCHEMA) {
const found = section.items.find((item) => item.key === key);
if (found) return found;
}
return null;
}
function clampNumberSetting(key, rawValue, min = 10, max = 999999) {
const fallback = Number(DEFAULTS[key]);
const n = parseFloat(rawValue);
const base = Number.isFinite(n) ? n : (Number.isFinite(fallback) ? fallback : min);
return Math.min(max, Math.max(min, base));
}
function removeProfileButtons() { document.querySelectorAll('.user-card-buttons').forEach((el) => el.remove()); }
function remountProfileButtons() { removeProfileButtons(); mountProfileButtons(); }
function showSettingsStatus(text) {
const el = document.querySelector('.ass-tm-save-status');
if (!el) return;
el.textContent = text;
window.clearTimeout(showSettingsStatus._timer);
showSettingsStatus._timer = window.setTimeout(() => { if (el.textContent === text) el.textContent = ''; }, 1800);
}
function applyRuntimeSettingChange(key) {
if (key === 'profileButtons' || key === 'profileButtonsConfig') {
remountProfileButtons();
} else if (key === 'cardNeedButtons') {
if (state.cardNeedButtons) scanCardNeedButtons();
else document.querySelectorAll('.show-need_button').forEach((el) => el.classList.remove('show-need_button'));
} else if (key === 'modalStarButton') {
if (state.modalStarButton) processExistingDialogs();
else document.querySelectorAll('.as-ext-star-meta-item').forEach((el) => el.remove());
} else if (key.startsWith('qn') || key === 'quickNavEnabled') {
if (key === 'quickNavEnabled') injectQuickNav();
else if (key === 'qnLinks') renderQnButtons();
else applyQnVisualSettings();
} else if (key.startsWith('profBtn') || key === 'profFontFamily') {
applyProfVisualSettings();
}
}
function setSettingFromUi(key, rawValue) {
const def = getSettingDef(key);
if (!def) return;
let next;
if (def.type === 'bool') next = !!rawValue;
else if (def.type === 'color' || def.type === 'text' || def.type === 'select') next = String(rawValue);
else next = clampNumberSetting(key, rawValue, def.min ?? 10, def.max ?? 999999);
gmSet(key, next);
applyRuntimeSettingChange(key);
}
function findTelegramHeaderButton() {
const candidates = Array.from(document.querySelectorAll('a, button')).filter((el) => el.id !== 'ass-tm-settings-fab');
const topRight = candidates
.map((el) => ({ el, rect: el.getBoundingClientRect?.() }))
.filter(({ rect }) => rect && rect.width > 0 && rect.height > 0 && rect.top >= 0 && rect.top < 190 && rect.right > Math.max(260, window.innerWidth * 0.55));
const isTelegram = (el) => {
const text = [el.getAttribute('href'), el.getAttribute('title'), el.className, el.textContent].join(' ').toLowerCase();
if (text.includes('telegram') || text.includes('t.me') || text.includes('tg://')) return true;
const icon = el.querySelector?.('i, svg');
return icon && (icon.className.includes('telegram') || icon.outerHTML.includes('paper-plane'));
};
const direct = topRight.find(({ el }) => isTelegram(el));
if (direct) return direct.el;
const smallButtons = topRight.filter(({ rect }) => rect.width <= 56 && rect.height <= 56).sort((a, b) => b.rect.right - a.rect.right || a.rect.top - b.rect.top);
return smallButtons[0]?.el || null;
}
function makeSettingsButton(replaceTarget = null) {
const btn = document.createElement('button');
btn.id = 'ass-tm-settings-fab';
btn.type = 'button';
btn.title = 'ASS Tools настройки';
btn.innerHTML = '<i class="fal fa-cog" aria-hidden="true"></i>';
if (replaceTarget) {
const cls = String(replaceTarget.getAttribute('class') || '').trim();
if (cls) btn.className = cls;
btn.classList.add('ass-tm-settings-header-btn');
} else {
btn.className = 'ass-tm-settings-fallback';
}
btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openSettingsPanel(); });
return btn;
}
function ensureSettingsFab() {
const existing = document.getElementById('ass-tm-settings-fab');
if (existing && !existing.classList.contains('ass-tm-settings-fallback')) return;
const target = findTelegramHeaderButton();
if (target && target.id !== 'ass-tm-settings-fab') {
const btn = makeSettingsButton(target);
target.replaceWith(btn);
if (existing && existing !== btn) existing.remove();
return;
}
if (existing) return;
document.body.appendChild(makeSettingsButton(null));
}
// --- Double-Click Confirm Utility ---
function addDoubleConfirm(btn, normalText, onConfirm) {
let clicks = 0;
btn.addEventListener('click', (e) => {
e.preventDefault();
if (clicks === 0) {
clicks++;
const originalBg = btn.style.background;
btn.textContent = 'Уверены? (Нажмите еще раз)';
btn.style.background = '#d93838';
setTimeout(() => {
if (clicks > 0) {
clicks = 0;
btn.textContent = normalText;
btn.style.background = originalBg;
}
}, 3000);
} else {
clicks = 0;
btn.textContent = normalText;
btn.style.background = '';
onConfirm();
}
});
}
// --- List Editors (Profile + Quick Nav Links) ---
function renderListEditor(panel, listClass, dataKey, defaults, rowBuilder, readRow) {
const list = panel.querySelector(listClass);
if (!list) return;
list.innerHTML = '';
const items = Array.isArray(state[dataKey]) && state[dataKey].length ? state[dataKey] : defaults;
items.forEach((item, index) => list.appendChild(rowBuilder(item, index)));
const saveFn = () => {
const config = Array.from(list.children).map((row, i) => readRow(row, i)).filter(Boolean);
gmSet(dataKey, config.length ? config : cloneValue(defaults));
applyRuntimeSettingChange(dataKey);
showSettingsStatus('Сохранено');
};
let timer = null;
const schedule = () => { clearTimeout(timer); timer = setTimeout(saveFn, 250); };
list.addEventListener('input', schedule);
list.addEventListener('change', schedule);
const wrapper = list.closest('.ass-tm-editor');
wrapper.addEventListener('click', (e) => {
if (e.target.closest('.ass-tm-editor-remove')) {
e.target.closest('.ass-tm-editor-row').remove();
saveFn();
} else if (e.target.dataset.action === 'add') {
list.appendChild(rowBuilder({}, Date.now()));
saveFn();
}
});
const resetBtn = wrapper.querySelector('[data-action="reset"]');
if (resetBtn && !resetBtn.dataset.bound) {
resetBtn.dataset.bound = '1';
addDoubleConfirm(resetBtn, 'Сбросить', () => {
gmSet(dataKey, cloneValue(defaults));
applyRuntimeSettingChange(dataKey);
renderListEditor(panel, listClass, dataKey, defaults, rowBuilder, readRow);
showToast('Элементы сброшены');
});
}
}
function buildProfileRow(item, i) {
const row = document.createElement('div'); row.className = 'ass-tm-editor-row'; row.dataset.id = item.id || `p-${i}`;
const enabled = document.createElement('input'); enabled.type = 'checkbox'; enabled.checked = item.enabled !== false; enabled.dataset.f = 'en'; enabled.title = 'Включено';
const text = document.createElement('input'); text.type = 'text'; text.value = item.text || ''; text.placeholder = 'Текст'; text.dataset.f = 'txt';
const icon = document.createElement('input'); icon.type = 'text'; icon.value = item.icon || ''; icon.placeholder = 'Icon'; icon.dataset.f = 'icn';
const rm = document.createElement('button'); rm.type = 'button'; rm.className = 'ass-tm-editor-remove'; rm.textContent = '×'; rm.title = 'Удалить';
const url = document.createElement('input'); url.type = 'text'; url.value = item.url || ''; url.placeholder = 'URL'; url.dataset.f = 'url';
// ВАЖНО: Добавление элементов ровно так, как указано на скриншоте пользователя
row.append(enabled, text, icon, rm, url);
return row;
}
function readProfileRow(row, i) {
const q = (s) => row.querySelector(`[data-f="${s}"]`)?.value?.trim() || '';
const url = q('url'), txt = q('txt'), icn = q('icn');
if (!url && !txt && !icn) return null;
return { id: row.dataset.id || `p-${i}`, enabled: !!row.querySelector('[data-f="en"]')?.checked, text: txt, icon: icn, url: url };
}
function buildQnRow(item, i) {
const row = document.createElement('div'); row.className = 'ass-tm-editor-row qn-link'; row.dataset.id = item.id || `q-${i}`;
const text = document.createElement('input'); text.type = 'text'; text.value = item.title || ''; text.placeholder = 'Название'; text.dataset.f = 'ttl';
const rm = document.createElement('button'); rm.type = 'button'; rm.className = 'ass-tm-editor-remove'; rm.textContent = '×';
const url = document.createElement('input'); url.type = 'text'; url.value = item.url || ''; url.placeholder = 'URL'; url.dataset.f = 'url';
// По аналогии: текст, удаление, url
row.append(text, rm, url);
return row;
}
function readQnRow(row, i) {
const url = row.querySelector('[data-f="url"]')?.value?.trim() || '';
const ttl = row.querySelector('[data-f="ttl"]')?.value?.trim() || '';
if (!url && !ttl) return null;
return { id: row.dataset.id || `q-${i}`, title: ttl || 'Без названия', url: url || '#' };
}
function openSettingsPanel() {
if (!document.body) return;
let backdrop = document.querySelector('.ass-tm-settings-backdrop');
if (!backdrop) {
backdrop = document.createElement('div');
backdrop.className = 'ass-tm-settings-backdrop';
document.body.appendChild(backdrop);
}
backdrop.innerHTML = '';
const panel = document.createElement('div');
panel.className = 'ass-tm-settings-panel';
panel.innerHTML = `
<div class="ass-tm-settings-head">
<div class="ass-tm-settings-title">ASS Tools — настройки</div>
<button type="button" class="ass-tm-settings-close" title="Закрыть">×</button>
</div>
<div class="ass-tm-settings-body">
<div class="ass-tm-settings-grid"></div>
<div class="ass-tm-editor" id="qn-editor">
<h3>Панель быстрого доступа (Ссылки)</h3>
<div class="ass-tm-editor-list"></div>
<div class="ass-tm-editor-actions">
<button type="button" data-action="add">Добавить ссылку</button>
<button type="button" data-action="reset">Сбросить</button>
</div>
</div>
<div class="ass-tm-editor" id="prof-editor">
<h3>Список кнопок профиля (включая ранги)</h3>
<p style="margin:0 0 8px; color:#999; font-size:11px;">Плейсхолдеры: {USERNAME}, {CLUB_ID}.</p>
<div class="ass-tm-editor-list"></div>
<div class="ass-tm-editor-actions">
<button type="button" data-action="add">Добавить кнопку</button>
<button type="button" data-action="reset">Сбросить</button>
</div>
</div>
<div class="ass-tm-save-status"></div>
<div class="ass-tm-settings-actions">
<button type="button" data-action="save">Сохранить всё</button>
<button type="button" data-action="reset-all" style="background:rgba(255,255,255,.05); color:#999;">Полный сброс настроек</button>
</div>
</div>
`;
const grid = panel.querySelector('.ass-tm-settings-grid');
SETTINGS_SCHEMA.forEach((section) => {
const sectionEl = document.createElement('section'); sectionEl.className = 'ass-tm-settings-section';
const h = document.createElement('h3'); h.textContent = section.title; sectionEl.appendChild(h);
section.items.forEach((item) => {
const row = document.createElement('label'); row.className = 'ass-tm-setting-row';
const text = document.createElement('span'); text.className = 'ass-tm-setting-label'; text.textContent = item.label;
let input;
if (item.type === 'select') {
input = document.createElement('select');
item.options.forEach(opt => {
const o = document.createElement('option');
o.value = opt.value;
o.textContent = opt.label;
input.appendChild(o);
});
input.value = state[item.key] || item.options[0].value;
} else {
input = document.createElement('input');
if (item.type === 'bool') {
input.type = 'checkbox'; input.checked = !!state[item.key];
} else if (item.type === 'color' || item.type === 'text') {
input.type = item.type; input.value = state[item.key] || '';
} else {
input.type = 'number'; input.min = String(item.min ?? 10); input.max = String(item.max ?? 999999);
if (item.step) input.step = item.step; else input.step = '1';
input.value = String(state[item.key]);
}
}
input.setAttribute('data-setting-key', item.key);
row.appendChild(text); row.appendChild(input); sectionEl.appendChild(row);
});
grid.appendChild(sectionEl);
});
renderListEditor(panel.querySelector('#qn-editor'), '.ass-tm-editor-list', 'qnLinks', DEFAULT_QN_LINKS, buildQnRow, readQnRow);
renderListEditor(panel.querySelector('#prof-editor'), '.ass-tm-editor-list', 'profileButtonsConfig', DEFAULT_PROFILE_BUTTONS, buildProfileRow, readProfileRow);
let numTimer = null;
panel.querySelectorAll('[data-setting-key]').forEach((input) => {
const saveOne = () => { setSettingFromUi(input.getAttribute('data-setting-key'), input.type === 'checkbox' ? input.checked : input.value); showSettingsStatus('Сохранено'); };
if (input.type === 'checkbox' || input.tagName === 'SELECT' || input.type === 'color') input.addEventListener('change', saveOne);
else { input.addEventListener('input', () => { window.clearTimeout(numTimer); numTimer = window.setTimeout(saveOne, 300); }); input.addEventListener('change', saveOne); }
});
panel.querySelector('.ass-tm-settings-close')?.addEventListener('click', () => backdrop.classList.remove('ass-tm-settings-open'));
panel.querySelector('[data-action="save"]')?.addEventListener('click', () => showSettingsStatus('Сохранено'));
const resetAllBtn = panel.querySelector('[data-action="reset-all"]');
if (resetAllBtn) {
addDoubleConfirm(resetAllBtn, 'Полный сброс настроек', () => {
Object.keys(DEFAULTS).forEach((k) => { gmSet(k, cloneValue(DEFAULTS[k])); applyRuntimeSettingChange(k); });
backdrop.classList.remove('ass-tm-settings-open');
showToast('Все настройки полностью сброшены');
openSettingsPanel();
});
}
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) backdrop.classList.remove('ass-tm-settings-open'); });
backdrop.appendChild(panel); backdrop.classList.add('ass-tm-settings-open');
}
// ---------------------------------------------------------------------------
// Profile buttons
// ---------------------------------------------------------------------------
function getProfileUsername() { return document.querySelector('.usn__name > h1')?.textContent?.trim?.() || ''; }
function getUserClubId() { const u = document.querySelector('.usn__club-item-top a')?.href; return u && u.includes('clubs/') ? u.replace(/\/$/, '').split('/').pop() : '1'; }
function resolveIconClass(ic) { return ic ? (ic.startsWith('fas ') ? `fal ${ic.slice(4)}` : ic) : ''; }
function mountProfileButtons() {
if (!state.profileButtons) return;
const username = getProfileUsername();
if (!username) return;
const header = document.querySelector('.usn-sect__header');
if (!header || header.querySelector('.user-card-buttons')) return;
const clubId = getUserClubId();
const box = document.createElement('div'); box.className = 'user-card-buttons';
(state.profileButtonsConfig || []).forEach((btn) => {
if (!btn.enabled) return;
const text = String(btn.text || '').trim(), iconClass = String(btn.icon || '').trim();
let url = String(btn.url || '').replace(/\{USERNAME\}/g, username).replace(/\{USER\}/g, username).replace(/\{CLUB_ID\}/g, clubId);
if (!url || (!text && !iconClass)) return;
if (!/^https?:\/\//i.test(url) && !url.startsWith('/')) url = `/${url.replace(/^\/+/, '')}`;
const link = document.createElement('a'); link.href = url; link.title = text || '';
if (iconClass) {
const i = document.createElement('i'); i.className = resolveIconClass(iconClass); link.appendChild(i);
if (text && text.length <= 3) link.appendChild(document.createTextNode(text));
} else link.textContent = text;
box.appendChild(link);
});
if (box.childNodes.length) header.appendChild(box);
}
// ---------------------------------------------------------------------------
// Card need buttons / class marker
// ---------------------------------------------------------------------------
const CARD_CONTAINER_SELECTOR = '.lootbox__card, .anime-cards__item, a.trade__main-item, a.history__body-item, .trade__inventory-item, div.trade__main-item, .remelt__inventory-item, .remelt__item, .anime-cards__placeholder, .stone__inventory-item';
function applyNeedButtonMarker(elm) {
if (!state.cardNeedButtons || !elm || elm.classList.contains('show-need_button')) return;
if (elm.classList.contains('show-trade_button') || elm.dataset?.canTrade === '1') return;
elm.classList.add('show-need_button');
}
function scanCardNeedButtons(root = document) { if (state.cardNeedButtons) root.querySelectorAll?.(CARD_CONTAINER_SELECTOR).forEach(applyNeedButtonMarker); }
// ---------------------------------------------------------------------------
// Modal star button
// ---------------------------------------------------------------------------
function addStarButton(modalContent) {
if (!state.modalStarButton || !modalContent) return;
const meta = modalContent.querySelector('.ncard__meta');
if (!meta || meta.querySelector('.as-ext-star-meta-item')) return;
const rankElement = modalContent.querySelector('.ncard__meta-item.ncard__rank');
if (!rankElement) return;
const rankClass = Array.from(rankElement.classList).find((c) => c.startsWith('rank-'));
const rank = rankClass ? rankClass.split('-').slice(1).join('-') : null;
const cardName = modalContent.querySelector('.anime-cards__name')?.textContent?.trim() || null;
if (!rank || !cardName) return;
const starLink = document.createElement('a');
starLink.href = `/update_stars/?rank=${encodeURIComponent(rank)}&search=${encodeURIComponent(cardName)}`;
starLink.className = 'ncard__meta-item as-ext-star-meta-item';
// Идеальная, залитая по центру SVG звезда (гарантированно работает без внешних шрифтов)
starLink.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="16" height="16" fill="gold"><path d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"/></svg>';
starLink.style.cssText = 'display:flex; align-items:center; justify-content:center; width:36px; min-width:36px; height:36px; border-radius:50%; text-decoration:none; padding:0; margin:0; box-sizing:border-box; background-color:transparent; border:1px solid #555; transition:all .2s ease;';
starLink.onmouseenter = () => { starLink.style.backgroundColor = 'rgba(158,41,79,.9)'; starLink.style.borderColor = 'rgba(158,41,79,.9)'; };
starLink.onmouseleave = () => { starLink.style.backgroundColor = 'transparent'; starLink.style.borderColor = '#555'; };
meta.style.columnGap = '5px';
meta.insertBefore(starLink, rankElement);
}
function processDialogElement(dialogElement) {
if (state.modalStarButton) setTimeout(() => addStarButton(dialogElement.querySelector?.('#card-modal .modal__content')), 50);
}
function processExistingDialogs() { document.querySelectorAll('.ui-dialog').forEach(processDialogElement); }
// ---------------------------------------------------------------------------
// Stones (Event-Driven Auto-collect)
// ---------------------------------------------------------------------------
function clickStoneWithDelay(stoneElement) {
const baseDelay = clampNumberSetting('stoneBaseDelayMs', state.stoneBaseDelayMs, 10, 5000);
const growthDelay = clampNumberSetting('stoneGrowthDelayMs', state.stoneGrowthDelayMs, 10, 3000);
const currentDelay = baseDelay + (growthDelay * stoneClickQueue);
stoneClickQueue++;
setTimeout(() => {
if (stoneElement && document.body.contains(stoneElement)) {
stoneElement.click();
}
stoneClickQueue--;
if (stoneClickQueue < 0) stoneClickQueue = 0;
}, currentDelay);
}
function scanExistingStones() {
if (state.takeCinemaStone) {
const diamonds = Array.from(document.querySelectorAll('#diamonds-chat[data-code]'));
diamonds.reverse().forEach((diamond) => {
if (diamond.dataset.assQueued === '1') return;
const code = diamond.getAttribute('data-code');
if (!code || clickedCinemaCodes.has(code)) return;
clickedCinemaCodes.add(code);
diamond.dataset.assQueued = '1';
clickStoneWithDelay(diamond);
});
}
if (state.takeSnowStone) {
const snow = document.querySelector('#snow-stone-gift');
if (snow && snow.dataset.assQueued !== '1') {
snow.dataset.assQueued = '1';
clickStoneWithDelay(snow);
}
}
if (state.takeHeavenlyStone) {
const heavenly = document.querySelector('#gift-icon');
if (heavenly && heavenly.dataset.assQueued !== '1') {
heavenly.dataset.assQueued = '1';
clickStoneWithDelay(heavenly);
}
}
}
function startInitialStoneScanner() {
let ticks = 0;
const maxTicks = 60;
scanExistingStones();
const scanInterval = setInterval(() => {
scanExistingStones();
ticks++;
if (ticks >= maxTicks) clearInterval(scanInterval);
}, 3000);
}
function interactionWithChat() {
if (!state.takeCinemaStone) return;
const idle = document.querySelector('#animesssChatIdle');
if (idle && idle.style.display !== 'none') {
document.querySelector('#animesssChatIdleBack')?.click();
document.activeElement?.focus?.();
}
}
// ---------------------------------------------------------------------------
// Main Observers & Init
// ---------------------------------------------------------------------------
function setupObservers() {
new MutationObserver((mutations) => {
for (const m of mutations) {
m.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches?.(CARD_CONTAINER_SELECTOR)) applyNeedButtonMarker(node);
scanCardNeedButtons(node);
if (node.classList?.contains('ui-dialog')) processDialogElement(node);
const nestedDialog = node.querySelector?.('.ui-dialog');
if (nestedDialog) processDialogElement(nestedDialog);
if (state.takeCinemaStone) {
let diamonds = [];
if (node.id === 'diamonds-chat' && node.hasAttribute('data-code')) {
diamonds.push(node);
} else if (node.querySelectorAll) {
diamonds = Array.from(node.querySelectorAll('#diamonds-chat[data-code]'));
}
diamonds.reverse().forEach((diamond) => {
if (diamond.dataset.assQueued === '1') return;
const code = diamond.getAttribute('data-code');
if (!code || clickedCinemaCodes.has(code)) return;
clickedCinemaCodes.add(code);
diamond.dataset.assQueued = '1';
clickStoneWithDelay(diamond);
});
}
if (state.takeSnowStone) {
const snow = node.id === 'snow-stone-gift' ? node : node.querySelector?.('#snow-stone-gift');
if (snow && snow.dataset.assQueued !== '1') {
snow.dataset.assQueued = '1';
clickStoneWithDelay(snow);
}
}
if (state.takeHeavenlyStone) {
const heavenly = node.id === 'gift-icon' ? node : node.querySelector?.('#gift-icon');
if (heavenly && heavenly.dataset.assQueued !== '1') {
heavenly.dataset.assQueued = '1';
clickStoneWithDelay(heavenly);
}
}
});
}
mountProfileButtons();
ensureSettingsFab();
if (state.quickNavEnabled && !document.getElementById('custom-quick-nav') && document.querySelector('header')) {
injectQuickNav();
}
}).observe(document.body, { childList: true, subtree: true });
}
function main() {
loadSettings();
injectStyle();
onBodyReady(() => {
injectQuickNav();
applyProfVisualSettings();
ensureSettingsFab();
setupObservers();
mountProfileButtons();
scanCardNeedButtons();
processExistingDialogs();
startInitialStoneScanner();
setInterval(interactionWithChat, 10000);
});
}
main();
})();