Автор |
Сообщение
|
admin
|
Всем привет! Можно улучшить пользовательский опыт с помощью скриптов. Инструкция:
1. Установить расширение к браузеру для запуска скриптов: Violentmonkey (рекомендуется) или Tampermonkey.
2. Создать в нем новый скрипт, вставить вместо него текст выбранного скрипта и сохранить.
3. Все скрипты рассчитаны на https://tormac.org - HTTP не поддерживается.
TorMac User Menu
Добавляет выпадающее меню со ссылками на сообщения пользователя в теме (и другие ссылки).
Код:
// ==UserScript==
// @name TorMac User Menu
// @namespace copyMister
// @version 1.4.1
// @description Adds a dropdown with quick search links for users in forum topics (e.g., messages by author). Note: Requires JavaScript enabled for tormac.org in Tor Browser (via NoScript or security settings).
// @description:ru Добавляет выпадающее меню со ссылками на сообщения пользователя в теме (и другие ссылки). Требуется включить JavaScript для tormac.org в Tor Browser (через NoScript или настройки безопасности).
// @author copyMister
// @license MIT
// @match https://tormac.org/viewtopic.php*
// @match https://tormac.org/torrent/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=tormac.org
// @run-at document-body
// @grant none
// @homepageURL https://tormac.org/torrent/uluchshenie-polzovatelskogo-opyta.5301
// ==/UserScript== (function() {
'use strict'; $.holdReady(true); document.addEventListener('DOMContentLoaded', function() {
if (!document.querySelector('#quick-search')) {
console.error('Quick search element not found');
$.holdReady(false);
return;
} // Extract topicId from URL (handles both viewtopic.php and /torrent/*)
let topicId;
const url = window.location.href;
if (url.includes('viewtopic.php')) {
topicId = new URLSearchParams(window.location.search).get('t');
} else if (url.includes('/torrent/')) {
const match = url.match(/\.(\d+)$/);
topicId = match ? match[1] : null;
} if (!topicId) {
console.error('Could not extract topicId from URL:', url);
$.holdReady(false);
return;
} // Extract forumId (assumes same selector as pornolab.net; adjust if needed)
const forumOption = document.querySelector('option[value^="search.php?f="]');
const forumId = forumOption ? forumOption.value.split('=')[1] : null; if (!forumId) {
console.error('Could not extract forumId');
$.holdReady(false);
return;
} // Process each post to add user menu
document.querySelectorAll('#topic_main .poster_btn.td3 > div').forEach(function(div) {
const userLink = div.firstElementChild.href;
const userIdMatch = userLink.match(/u=(\d+)/);
const userId = userIdMatch ? userIdMatch[1] : null;
if (!userId) return; const userName = div.closest('tbody').querySelector('.poster_info > .nick')?.textContent.trim();
if (!userName) {
console.error('Could not extract userName for userId:', userId);
return;
} const userMenu = `usermenu-${userId}`; // Add dropdown toggle link
div.insertAdjacentHTML(
'beforeend',
`<a class="txtb menu-root without-caret" href="#${userMenu}">▼</a>`
); // Add dropdown menu if not already present
if (!document.querySelector(`#${userMenu}`)) {
document.body.insertAdjacentHTML(
'beforeend',
`<div id="${userMenu}" class="menu-sub"><div class="menu-a bold nowrap">
<table style="width: 100%; border-collapse: collapse; border: 0;">
<tr><th style="font-size: 12px; padding: 4px 12px;">${userName}</th></tr>
</table>
<a class="med" href="search.php?uid=${userId}&search_author=1&only_replies">Все ответы</a>
<a class="med" href="search.php?uid=${userId}&t=${topicId}&dm=1">Сообщения в этой теме</a>
<a class="med" href="search.php?uid=${userId}&f=${forumId}&dm=1">Сообщения в этом разделе</a>
<a class="med" href="search.php?uid=${userId}&search_author=1">Сообщения по всему трекеру</a>
<a class="med" href="search.php?uid=${userId}&myt=1">Начатые темы</a>
<a class="med" href="tracker.php?rid=${userId}">Раздачи</a></div></div>`
);
}
}); $.holdReady(false);
});
})();
TorMac Preloaded Preview
Код:
// ==UserScript==
// @name TorMac Preloaded Preview
// @version 1.7.0
// @description Прелоадит и отображает превью изображений под ссылками на tormac.org для страниц трекера, поиска и форумов.
// @author Ace
// @license MIT
// @match *://tormac.org/search*
// @match *://tormac.org/tracker*
// @match *://tormac.org/viewforum*
// @match *://tormac.org/torrent/*
// @match *://tormac.org/forum/*
// @run-at document-end
// @require https://cdnjs.cloudflare.com/ajax/libs/core-js/3.25.1/minified.js
// @grant none
// @namespace https://tormac.org/torrent/uluchshenie-polzovatelskogo-opyta.5301
// ==/UserScript== (async function(){
const previewSize = {width: "auto", height: "17rem"};
const inProgressLinks = new Set();
const failedLinks = new Map();
const MAX_RETRIES = 3;
const THROTTLE_INTERVAL = 300;
const MAX_CONCURRENT_FETCHES = 5;
let concurrentFetchCount = 0; try {
const style = document.createElement("style");
style.textContent = `.preview-container{max-width:${previewSize.width};height:${previewSize.height};display:flex;justify-content:center;align-items:center;overflow:hidden}.preview-container img{width:100%;height:100%;object-fit:scale-down}`;
document.head.appendChild(style);
} catch (error) {} const previewCache = new Map(); async function fetchWithRetry(url, retries = 0) {
try {
const response = await fetch(url, { timeout: 10000 });
if (!response.ok) {
if (retries < MAX_RETRIES) return fetchWithRetry(url, retries + 1);
failedLinks.set(url, `Failed after ${MAX_RETRIES} retries`);
return [];
}
const html = await response.text();
const doc = new DOMParser().parseFromString(html, "text/html");
const imgElements = doc.querySelectorAll(".postImg, .postimage, img[title], var.postImg img");
const imgUrls = Array.from(imgElements).map(img => img.title || img.src || img.dataset.src).filter(Boolean);
if (imgUrls.length === 0) failedLinks.set(url, "No images found");
return imgUrls;
} catch (error) {
if (retries < MAX_RETRIES) return fetchWithRetry(url, retries + 1);
failedLinks.set(url, `Failed after ${MAX_RETRIES} retries`);
return [];
}
} function normalizeUrl(href) {
const baseUrl = "https://tormac.org";
if (!href) return null;
if (href.startsWith("/")) return baseUrl + href;
if (href.match(/\/(torrent|forum)\/.+/)) {
const match = href.match(/\.(\d+)$/);
const type = href.includes("/torrent") ? "torrent" : "forum";
if (match) {
const id = match[1];
return type === "torrent" ? `${baseUrl}/viewtopic.php?t=${id}` : `${baseUrl}/viewforum.php?f=${id}`;
}
return href;
}
return href.startsWith(baseUrl) ? href : baseUrl + "/" + href;
} async function getCachedOrFetchPreviewUrls(linkElement) {
const href = linkElement.getAttribute("href");
const url = normalizeUrl(href);
if (!url) return [];
if (previewCache.has(url)) return Promise.resolve(previewCache.get(url));
if (inProgressLinks.has(url) || failedLinks.has(url)) return Promise.resolve([]);
inProgressLinks.add(url);
const delay = () => new Promise(resolve => setTimeout(resolve, 100));
while (concurrentFetchCount >= MAX_CONCURRENT_FETCHES) await delay();
concurrentFetchCount++;
try {
const result = await fetchWithRetry(url);
previewCache.set(url, result);
return result;
} finally {
concurrentFetchCount--;
inProgressLinks.delete(url);
}
} function insertPreview(linkElement, imgUrls) {
try {
if (!imgUrls || imgUrls.length === 0 || linkElement.hasAttribute("data-preview-processed")) return;
linkElement.setAttribute("data-preview-processed", "true");
if (linkElement.nextElementSibling?.classList.contains("preview-container")) return;
const previewContainer = document.createElement("div");
previewContainer.className = "preview-container";
const img = document.createElement("img");
img.src = imgUrls[0];
img.loading = "lazy";
img.addEventListener("error", function() {
imgUrls.shift();
if (imgUrls.length > 0) img.src = imgUrls[0];
else img.remove();
});
img.addEventListener("load", function() {
if (img.naturalHeight < 201) {
imgUrls.shift();
if (imgUrls.length > 0) img.src = imgUrls[0];
else img.remove();
}
});
previewContainer.appendChild(img);
linkElement.parentNode.insertBefore(previewContainer, linkElement.nextSibling);
linkElement.parentNode.style.cssText = "padding:1rem;display:flex;justify-content:space-between";
linkElement.style.cssText = "width:30%";
} catch (error) {}
} async function preloadPreviews() {
try {
const links = document.querySelectorAll(".tLink, .tt-text, .topictitle");
const tasks = Array.from(links).map(link => async () => {
const imgUrls = await getCachedOrFetchPreviewUrls(link);
insertPreview(link, imgUrls);
});
await preloadAll(tasks, 5);
} catch (error) {}
} async function preloadAll(tasks, limit = 5) {
const results = [];
try {
while (tasks.length) {
const batch = tasks.splice(0, limit).map(task => task());
results.push(...(await Promise.allSettled(batch)));
}
} catch (error) {}
return results;
} function throttle(func, limit) {
let lastFunc;
let lastRan;
return function(...args) {
const context = this;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if (Date.now() - lastRan >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
} try {
const throttledObserverCallback = throttle(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const link = entry.target;
preloadPreviews().then(() => observer.unobserve(link)).catch(() => observer.observe(link));
}
});
}, THROTTLE_INTERVAL);
const observer = new IntersectionObserver(throttledObserverCallback);
const observeLinks = () => {
document.querySelectorAll(".tLink, .tt-text, .topictitle").forEach(link => {
if (!link.hasAttribute("data-preview-processed")) observer.observe(link);
});
};
observeLinks();
const mutationObserver = new MutationObserver(observeLinks);
mutationObserver.observe(document.body, { childList: true, subtree: true });
} catch (error) {}
})();
TorMac Preview (только Violentmonkey)
Код:
// ==UserScript==
// @name TorMac Preview
// @namespace http://tampermonkey.net/
// @version 1.1.2
// @description Предварительный просмотр скриншотов на tormac.org
// @author С
// @match https://tormac.org/*
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript== (function() {
'use strict'; //====================================
// НАСТРОЙКИ
//==================================== const defaultSettings = {
previewThumbnailSize: 100,
lightboxThumbnailSize: 800,
previewMaxWidth: 500,
previewMaxHeight: 500,
previewGridColumns: 3,
maxThumbnailsBeforeSpoiler: 12,
previewPosition: 'bottomLeft',
colorTheme: 'light',
hoverEffectTime: 0.3,
previewHideDelay: 300,
enableAutoPreview: true,
hidePreviewIfEmpty: true,
neverUseSpoilers: false,
siteSettings: {
tormac: {
enabled: true,
useFullSizeInLightbox: true,
clickBehavior: 'lightbox',
supportedHostings: ['umpic', 'fastpic', 'imagebam', 'imgbox', 'imageban', 'imgur', 'postimage']
}
},
navButtonsSize: 60,
navButtonsVisibility: 'hover',
keyboardShortcuts: {
close: 'Escape',
prev: 'ArrowLeft',
next: 'ArrowRight',
reset: 'Home',
fullscreen: 'F'
},
cacheTTL: 3600,
maxCacheSize: 50,
enableLogging: false
}; function loadSettings() {
const savedSettings = GM_getValue('tormacPreviewSettings');
let settings = Object.assign({}, defaultSettings); if (savedSettings) {
try {
const parsed = JSON.parse(savedSettings);
settings = mergeDeep(settings, parsed);
} catch (e) {
console.error('Ошибка при загрузке настроек:', e);
}
} return settings;
} function saveSettings(settings) {
GM_setValue('tormacPreviewSettings', JSON.stringify(settings));
} function mergeDeep(target, source) {
const isObject = obj => obj && typeof obj === 'object' && !Array.isArray(obj);
if (!isObject(target) || !isObject(source)) {
return source;
} Object.keys(source).forEach(key => {
const targetValue = target[key];
const sourceValue = source[key]; if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
target[key] = sourceValue;
} else if (isObject(targetValue) && isObject(sourceValue)) {
target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue);
} else {
target[key] = sourceValue;
}
}); return target;
} const settings = loadSettings(); function log(...args) {
if (settings.enableLogging) {
const timestamp = new Date().toLocaleTimeString();
console.log(`[${timestamp}] tormac Preview:`, ...args);
}
} //====================================
// ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
//==================================== function createElement(tag, properties = {}, styles = {}) {
const element = document.createElement(tag);
Object.assign(element, properties);
Object.assign(element.style, styles);
return element;
} function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
} function addHoverEffect(element, imgElement) {
imgElement.style.transition = `transform ${settings.hoverEffectTime}s ease`;
element.addEventListener('mouseenter', () => {
imgElement.style.transform = 'scale(1.05)';
});
element.addEventListener('mouseleave', () => {
imgElement.style.transform = 'scale(1)';
});
} function createThumbnail(imgData, openImageFunc, siteName) {
const aElement = createElement('a', { href: imgData.fullUrl });
const clickBehavior = settings.siteSettings[siteName].clickBehavior; aElement.addEventListener('click', function(e) {
e.preventDefault();
if (clickBehavior === 'lightbox') {
openImageFunc(imgData.thumbUrl, imgData.fullUrl);
} else {
window.open(imgData.fullUrl, '_blank');
}
}); const imgElement = createElement('img', {
src: imgData.thumbUrl,
alt: 'Thumbnail'
}, {
maxWidth: '100%',
maxHeight: `${settings.previewThumbnailSize}px`,
objectFit: 'cover',
borderRadius: '4px'
}); addHoverEffect(aElement, imgElement);
aElement.appendChild(imgElement);
return aElement;
} function addImagesToContainer(container, imageLinks, openImageFunc, siteName, startIndex = 0, endIndex = imageLinks.length) {
const links = imageLinks.slice(startIndex, endIndex);
links.forEach(imgData => {
const thumbnail = createThumbnail(imgData, openImageFunc, siteName);
container.appendChild(thumbnail);
});
} //====================================
// ОКНО НАСТРОЕК
//==================================== const settingsDialogHTML = `
<div id="tormac-preview-settings-backdrop" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 10000; display: flex; justify-content: center; align-items: center;">
<div id="tormac-preview-settings-dialog" style="background-color: var(--bg-color); border-radius: 8px; padding: 20px; max-width: 800px; width: 90%; max-height: 90vh; overflow-y: auto; position: relative; color: var(--text-color);">
<style>
:root {
--bg-color: ${settings.colorTheme === 'dark' ? '#222' : 'white'};
--text-color: ${settings.colorTheme === 'dark' ? '#eee' : 'black'};
--border-color: ${settings.colorTheme === 'dark' ? '#444' : '#ccc'};
}
#tormac-preview-settings-dialog input, #tormac-preview-settings-dialog select, #tormac-preview-settings-dialog button {
background-color: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 5px;
}
#tormac-preview-settings-dialog button:hover {
background-color: var(--border-color);
}
</style>
<h2 style="margin-top: 0; border-bottom: 1px solid var(--border-color); padding-bottom: 10px;">Настройки tormac Preview</h2>
<div style="position: absolute; top: 20px; right: 20px; cursor: pointer; font-size: 24px; font-weight: bold;" id="tormac-preview-settings-close">×</div>
<div style="display: flex; flex-wrap: wrap; gap: 20px;">
<div style="flex: 1; min-width: 300px;">
<h3>Размеры и внешний вид</h3>
<div style="margin-bottom: 15px;">
<label for="previewThumbnailSize">Размер миниатюр в окне предпросмотра (px):</label>
<input type="range" id="previewThumbnailSize" min="50" max="500" step="10" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>50</span>
<span id="previewThumbnailSizeValue">100</span>
<span>500</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="lightboxThumbnailSize">Размер изображений в лайтбоксе (px):</label>
<input type="range" id="lightboxThumbnailSize" min="400" max="1500" step="100" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>400</span>
<span id="lightboxThumbnailSizeValue">800</span>
<span>1500</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="previewMaxWidth">Максимальная ширина окна предпросмотра (px):</label>
<input type="range" id="previewMaxWidth" min="200" max="1000" step="50" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>200</span>
<span id="previewMaxWidthValue">500</span>
<span>1000</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="previewMaxHeight">Максимальная высота окна предпросмотра (px):</label>
<input type="range" id="previewMaxHeight" min="200" max="1000" step="50" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>200</span>
<span id="previewMaxHeightValue">500</span>
<span>1000</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="previewGridColumns">Количество столбцов в сетке миниатюр:</label>
<input type="range" id="previewGridColumns" min="1" max="8" step="1" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>1</span>
<span id="previewGridColumnsValue">3</span>
<span>8</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="maxThumbnailsBeforeSpoiler">Макс. количество миниатюр до спойлера:</label>
<input type="range" id="maxThumbnailsBeforeSpoiler" min="3" max="50" step="1" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>3</span>
<span id="maxThumbnailsBeforeSpoilerValue">12</span>
<span>50</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="previewPosition">Положение окна предпросмотра:</label>
<select id="previewPosition" style="width: 100%;">
<option value="bottomRight">Снизу справа</option>
<option value="bottomLeft">Снизу слева</option>
<option value="topRight">Сверху справа</option>
<option value="topLeft">Сверху слева</option>
</select>
</div>
<div style="margin-bottom: 15px;">
<label for="colorTheme">Цветовая схема:</label>
<select id="colorTheme" style="width: 100%;">
<option value="light">Светлая</option>
<option value="dark">Темная</option>
<option value="system">Системная</option>
</select>
</div>
</div>
<div style="flex: 1; min-width: 300px;">
<h3>Поведение</h3>
<div style="margin-bottom: 15px;">
<label for="previewHideDelay">Задержка перед скрытием окна предпросмотра (мс):</label>
<input type="range" id="previewHideDelay" min="100" max="2000" step="100" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>100</span>
<span id="previewHideDelayValue">300</span>
<span>2000</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="hoverEffectTime">Время анимации эффекта наведения (сек):</label>
<input type="range" id="hoverEffectTime" min="0.1" max="1.0" step="0.1" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>0.1</span>
<span id="hoverEffectTimeValue">0.3</span>
<span>1.0</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label>
<input type="checkbox" id="enableAutoPreview">
Включить окно предпросмотра
</label>
</div>
<div style="margin-bottom: 15px;">
<label>
<input type="checkbox" id="hidePreviewIfEmpty">
Не показывать окно предпросмотра, если нет скриншотов
</label>
</div>
<div style="margin-bottom: 15px;">
<label>
<input type="checkbox" id="neverUseSpoilers">
Никогда не скрывать изображения под спойлер
</label>
</div>
<h3>Настройки сайта</h3>
<div style="margin-bottom: 15px;">
<label>
<input type="checkbox" id="tormacEnabled">
Включить для tormac
</label>
</div>
<div style="margin-bottom: 15px; margin-left: 20px;">
<label>
<input type="checkbox" id="tormacUseFullSize">
Полные изображения в лайтбоксе
</label>
</div>
<div style="margin-bottom: 15px; margin-left: 20px;">
<label for="tormacClickBehavior">Поведение при клике на изображение:</label>
<select id="tormacClickBehavior" style="width: 100%;">
<option value="lightbox">Открывать в лайтбоксе</option>
<option value="newTab">Открывать в новой вкладке</option>
</select>
</div>
<div style="margin-bottom: 15px; margin-left: 20px;">
<label>Поддерживаемые хостинги:</label>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
${['umpic', 'fastpic', 'imagebam', 'imgbox', 'imageban', 'imgur', 'postimage'].map(hosting => `
<label style="flex: 1 1 45%;">
<input type="checkbox" name="supportedHostings" value="${hosting}" ${settings.siteSettings.tormac.supportedHostings.includes(hosting) ? 'checked' : ''}>
${hosting.charAt(0).toUpperCase() + hosting.slice(1)}
</label>
`).join('')}
</div>
</div>
</div>
<div style="flex: 1; min-width: 300px;">
<h3>Кнопки навигации</h3>
<div style="margin-bottom: 15px;">
<label for="navButtonsSize">Размер кнопок навигации (px):</label>
<input type="range" id="navButtonsSize" min="30" max="100" step="5" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>30</span>
<span id="navButtonsSizeValue">60</span>
<span>100</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="navButtonsVisibility">Видимость кнопок навигации:</label>
<select id="navButtonsVisibility" style="width: 100%;">
<option value="always">Всегда видимы</option>
<option value="hover">Видимы при наведении</option>
<option value="never">Всегда скрыты</option>
</select>
</div>
<h3>Горячие клавиши</h3>
<div style="margin-bottom: 15px;">
<label for="closeKey">Закрытие лайтбокса:</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="closeKey" style="width: 100%;" readonly>
<button id="changeCloseKey">Изменить</button>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="prevKey">Предыдущее изображение:</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="prevKey" style="width: 100%;" readonly>
<button id="changePrevKey">Изменить</button>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="nextKey">Следующее изображение:</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="nextKey" style="width: 100%;" readonly>
<button id="changeNextKey">Изменить</button>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="resetKey">Сброс позиции и масштаба:</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="resetKey" style="width: 100%;" readonly>
<button id="changeResetKey">Изменить</button>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="fullscreenKey">Полноэкранный режим:</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="fullscreenKey" style="width: 100%;" readonly>
<button id="changeFullscreenKey">Изменить</button>
</div>
</div>
<h3>Кэширование</h3>
<div style="margin-bottom: 15px;">
<label for="cacheTTL">Время жизни кэша (сек):</label>
<input type="range" id="cacheTTL" min="300" max="7200" step="300" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>300</span>
<span id="cacheTTLValue">3600</span>
<span>7200</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<label for="maxCacheSize">Максимальный размер кэша:</label>
<input type="range" id="maxCacheSize" min="10" max="100" step="10" style="width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span>10</span>
<span id="maxCacheSizeValue">50</span>
<span>100</span>
</div>
</div>
<h3>Отладка</h3>
<div style="margin-bottom: 15px;">
<label>
<input type="checkbox" id="enableLogging">
Включить логирование
</label>
</div>
<div style="margin-top: 30px; display: flex; gap: 10px; justify-content: space-between;">
<button id="saveSettings" style="padding: 8px 15px; background-color: #4CAF50; color: white; border: none; cursor: pointer;">Сохранить настройки</button>
<button id="resetSettings" style="padding: 8px 15px; background-color: #f44336; color: white; border: none; cursor: pointer;">Сбросить настройки</button>
</div>
</div>
</div>
</div>
</div>
`; function openSettingsDialog() {
if (document.getElementById('tormac-preview-settings-backdrop')) return; const dialogContainer = createElement('div', { innerHTML: settingsDialogHTML });
document.body.appendChild(dialogContainer); const elements = {
previewThumbnailSize: document.getElementById('previewThumbnailSize'),
previewThumbnailSizeValue: document.getElementById('previewThumbnailSizeValue'),
lightboxThumbnailSize: document.getElementById('lightboxThumbnailSize'),
lightboxThumbnailSizeValue: document.getElementById('lightboxThumbnailSizeValue'),
previewMaxWidth: document.getElementById('previewMaxWidth'),
previewMaxWidthValue: document.getElementById('previewMaxWidthValue'),
previewMaxHeight: document.getElementById('previewMaxHeight'),
previewMaxHeightValue: document.getElementById('previewMaxHeightValue'),
previewGridColumns: document.getElementById('previewGridColumns'),
previewGridColumnsValue: document.getElementById('previewGridColumnsValue'),
maxThumbnailsBeforeSpoiler: document.getElementById('maxThumbnailsBeforeSpoiler'),
maxThumbnailsBeforeSpoilerValue: document.getElementById('maxThumbnailsBeforeSpoilerValue'),
previewPosition: document.getElementById('previewPosition'),
colorTheme: document.getElementById('colorTheme'),
previewHideDelay: document.getElementById('previewHideDelay'),
previewHideDelayValue: document.getElementById('previewHideDelayValue'),
hoverEffectTime: document.getElementById('hoverEffectTime'),
hoverEffectTimeValue: document.getElementById('hoverEffectTimeValue'),
enableAutoPreview: document.getElementById('enableAutoPreview'),
hidePreviewIfEmpty: document.getElementById('hidePreviewIfEmpty'),
neverUseSpoilers: document.getElementById('neverUseSpoilers'),
tormacEnabled: document.getElementById('tormacEnabled'),
tormacUseFullSize: document.getElementById('tormacUseFullSize'),
tormacClickBehavior: document.getElementById('tormacClickBehavior'),
supportedHostings: document.querySelectorAll('input[name="supportedHostings"]'),
navButtonsSize: document.getElementById('navButtonsSize'),
navButtonsSizeValue: document.getElementById('navButtonsSizeValue'),
navButtonsVisibility: document.getElementById('navButtonsVisibility'),
closeKey: document.getElementById('closeKey'),
prevKey: document.getElementById('prevKey'),
nextKey: document.getElementById('nextKey'),
resetKey: document.getElementById('resetKey'),
fullscreenKey: document.getElementById('fullscreenKey'),
changeCloseKey: document.getElementById('changeCloseKey'),
changePrevKey: document.getElementById('changePrevKey'),
changeNextKey: document.getElementById('changeNextKey'),
changeResetKey: document.getElementById('changeResetKey'),
changeFullscreenKey: document.getElementById('changeFullscreenKey'),
cacheTTL: document.getElementById('cacheTTL'),
cacheTTLValue: document.getElementById('cacheTTLValue'),
maxCacheSize: document.getElementById('maxCacheSize'),
maxCacheSizeValue: document.getElementById('maxCacheSizeValue'),
enableLogging: document.getElementById('enableLogging'),
saveSettings: document.getElementById('saveSettings'),
resetSettings: document.getElementById('resetSettings'),
closeButton: document.getElementById('tormac-preview-settings-close')
}; for (const [key, element] of Object.entries(elements)) {
if (!element && key !== 'supportedHostings') {
log(`Ошибка: Элемент с ID ${key} не найден в DOM`);
alert(`Ошибка: Элемент с ID ${key} не найден. Проверьте HTML-разметку окна настроек.`);
closeSettingsDialog();
return;
}
} elements.previewThumbnailSize.value = settings.previewThumbnailSize;
elements.previewThumbnailSizeValue.textContent = settings.previewThumbnailSize;
elements.lightboxThumbnailSize.value = settings.lightboxThumbnailSize;
elements.lightboxThumbnailSizeValue.textContent = settings.lightboxThumbnailSize;
elements.previewMaxWidth.value = settings.previewMaxWidth;
elements.previewMaxWidthValue.textContent = settings.previewMaxWidth;
elements.previewMaxHeight.value = settings.previewMaxHeight;
elements.previewMaxHeightValue.textContent = settings.previewMaxHeight;
elements.previewGridColumns.value = settings.previewGridColumns;
elements.previewGridColumnsValue.textContent = settings.previewGridColumns;
elements.maxThumbnailsBeforeSpoiler.value = settings.maxThumbnailsBeforeSpoiler;
elements.maxThumbnailsBeforeSpoilerValue.textContent = settings.maxThumbnailsBeforeSpoiler;
elements.previewPosition.value = settings.previewPosition;
elements.colorTheme.value = settings.colorTheme;
elements.previewHideDelay.value = settings.previewHideDelay;
elements.previewHideDelayValue.textContent = settings.previewHideDelay;
elements.hoverEffectTime.value = settings.hoverEffectTime;
elements.hoverEffectTimeValue.textContent = settings.hoverEffectTime;
elements.enableAutoPreview.checked = settings.enableAutoPreview;
elements.hidePreviewIfEmpty.checked = settings.hidePreviewIfEmpty;
elements.neverUseSpoilers.checked = settings.neverUseSpoilers;
elements.tormacEnabled.checked = settings.siteSettings.tormac.enabled;
elements.tormacUseFullSize.checked = settings.siteSettings.tormac.useFullSizeInLightbox;
elements.tormacClickBehavior.value = settings.siteSettings.tormac.clickBehavior;
elements.navButtonsSize.value = settings.navButtonsSize;
elements.navButtonsSizeValue.textContent = settings.navButtonsSize;
elements.navButtonsVisibility.value = settings.navButtonsVisibility;
elements.closeKey.value = settings.keyboardShortcuts.close;
elements.prevKey.value = settings.keyboardShortcuts.prev;
elements.nextKey.value = settings.keyboardShortcuts.next;
elements.resetKey.value = settings.keyboardShortcuts.reset;
elements.fullscreenKey.value = settings.keyboardShortcuts.fullscreen;
elements.cacheTTL.value = settings.cacheTTL;
elements.cacheTTLValue.textContent = settings.cacheTTL;
elements.maxCacheSize.value = settings.maxCacheSize;
elements.maxCacheSizeValue.textContent = settings.maxCacheSize;
elements.enableLogging.checked = settings.enableLogging; elements.previewThumbnailSize.addEventListener('input', () => {
elements.previewThumbnailSizeValue.textContent = elements.previewThumbnailSize.value;
});
elements.lightboxThumbnailSize.addEventListener('input', () => {
elements.lightboxThumbnailSizeValue.textContent = elements.lightboxThumbnailSize.value;
});
elements.previewMaxWidth.addEventListener('input', () => {
elements.previewMaxWidthValue.textContent = elements.previewMaxWidth.value;
});
elements.previewMaxHeight.addEventListener('input', () => {
elements.previewMaxHeightValue.textContent = elements.previewMaxHeight.value;
});
elements.previewGridColumns.addEventListener('input', () => {
elements.previewGridColumnsValue.textContent = elements.previewGridColumns.value;
});
elements.maxThumbnailsBeforeSpoiler.addEventListener('input', () => {
elements.maxThumbnailsBeforeSpoilerValue.textContent = elements.maxThumbnailsBeforeSpoiler.value;
});
elements.previewHideDelay.addEventListener('input', () => {
elements.previewHideDelayValue.textContent = elements.previewHideDelay.value;
});
elements.hoverEffectTime.addEventListener('input', () => {
elements.hoverEffectTimeValue.textContent = elements.hoverEffectTime.value;
});
elements.navButtonsSize.addEventListener('input', () => {
elements.navButtonsSizeValue.textContent = elements.navButtonsSize.value;
});
elements.cacheTTL.addEventListener('input', () => {
elements.cacheTTLValue.textContent = elements.cacheTTL.value;
});
elements.maxCacheSize.addEventListener('input', () => {
elements.maxCacheSizeValue.textContent = elements.maxCacheSize.value;
}); function setupKeyChangeHandler(keyField, changeButton) {
changeButton.addEventListener('click', () => {
const originalText = changeButton.textContent;
keyField.value = 'Нажмите клавишу...';
changeButton.textContent = 'Отмена'; const keyHandler = (e) => {
e.preventDefault();
keyField.value = e.key;
document.removeEventListener('keydown', keyHandler);
changeButton.textContent = originalText;
}; document.addEventListener('keydown', keyHandler); changeButton.addEventListener('click', () => {
document.removeEventListener('keydown', keyHandler);
changeButton.textContent = originalText;
keyField.value = settings.keyboardShortcuts[keyField.id.replace('Key', '')];
}, { once: true });
});
} setupKeyChangeHandler(elements.closeKey, elements.changeCloseKey);
setupKeyChangeHandler(elements.prevKey, elements.changePrevKey);
setupKeyChangeHandler(elements.nextKey, elements.changeNextKey);
setupKeyChangeHandler(elements.resetKey, elements.changeResetKey);
setupKeyChangeHandler(elements.fullscreenKey, elements.changeFullscreenKey); elements.saveSettings.addEventListener('click', () => {
const newSettings = {
previewThumbnailSize: parseInt(elements.previewThumbnailSize.value),
lightboxThumbnailSize: parseInt(elements.lightboxThumbnailSize.value),
previewMaxWidth: parseInt(elements.previewMaxWidth.value),
previewMaxHeight: parseInt(elements.previewMaxHeight.value),
previewGridColumns: parseInt(elements.previewGridColumns.value),
maxThumbnailsBeforeSpoiler: parseInt(elements.maxThumbnailsBeforeSpoiler.value),
previewPosition: elements.previewPosition.value,
colorTheme: elements.colorTheme.value,
previewHideDelay: parseInt(elements.previewHideDelay.value),
hoverEffectTime: parseFloat(elements.hoverEffectTime.value),
enableAutoPreview: elements.enableAutoPreview.checked,
hidePreviewIfEmpty: elements.hidePreviewIfEmpty.checked,
neverUseSpoilers: elements.neverUseSpoilers.checked,
siteSettings: {
tormac: {
enabled: elements.tormacEnabled.checked,
useFullSizeInLightbox: elements.tormacUseFullSize.checked,
clickBehavior: elements.tormacClickBehavior.value,
supportedHostings: Array.from(elements.supportedHostings)
.filter(input => input.checked)
.map(input => input.value)
}
},
navButtonsSize: parseInt(elements.navButtonsSize.value),
navButtonsVisibility: elements.navButtonsVisibility.value,
keyboardShortcuts: {
close: elements.closeKey.value,
prev: elements.prevKey.value,
next: elements.nextKey.value,
reset: elements.resetKey.value,
fullscreen: elements.fullscreenKey.value
},
cacheTTL: parseInt(elements.cacheTTL.value),
maxCacheSize: parseInt(elements.maxCacheSize.value),
enableLogging: elements.enableLogging.checked
}; saveSettings(newSettings);
Object.assign(settings, newSettings);
closeSettingsDialog();
}); elements.resetSettings.addEventListener('click', () => {
if (confirm('Вы уверены, что хотите сбросить все настройки на значения по умолчанию?')) {
saveSettings(defaultSettings);
Object.assign(settings, defaultSettings);
closeSettingsDialog();
}
}); elements.closeButton.addEventListener('click', closeSettingsDialog); const backdrop = document.getElementById('tormac-preview-settings-backdrop');
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
closeSettingsDialog();
}
});
} function closeSettingsDialog() {
const dialog = document.getElementById('tormac-preview-settings-backdrop');
if (dialog) dialog.remove();
} //====================================
// САЙТОЗАВИСИМЫЕ НАСТРОЙКИ
//==================================== const siteSpecificFunctions = {
tormac: {
getScreenshotLinks: function(spoilerElement) {
const links = [];
const aElements = spoilerElement.querySelectorAll('a[href]');
aElements.forEach(link => {
const imgElement = link.querySelector('img');
const varElement = link.querySelector('var.postImg');
if (imgElement && imgElement.src) {
const fullUrl = varElement && varElement.getAttribute('title') ?
varElement.getAttribute('title').split('?')[0] :
link.href.split('?')[0];
const thumbUrl = imgElement.src.split('?')[0];
if (fullUrl.match(/\.(jpg|jpeg|png|webp|avif)$/i)) {
links.push({ fullUrl, thumbUrl });
}
}
});
return links;
}, getScreenshotsFromPost: function(postElement) {
const links = [];
const processedUrls = new Set(); // Множество для отслеживания обработанных ссылок // Сбор ссылок из спойлеров
const spWrapSpoilers = postElement.querySelectorAll('div.sp-wrap, div.spoiler, div.sp-body');
spWrapSpoilers.forEach(spoiler => {
const aElements = spoiler.querySelectorAll('a[href]');
aElements.forEach(link => {
const imgElement = link.querySelector('img');
const varElement = link.querySelector('var.postImg');
if (imgElement && imgElement.src) {
const fullUrl = varElement && varElement.getAttribute('title') ?
varElement.getAttribute('title').split('?')[0] :
link.href.split('?')[0];
const thumbUrl = imgElement.src.split('?')[0];
if (fullUrl.match(/\.(jpg|jpeg|png|webp|avif)$/i) && !processedUrls.has(fullUrl)) {
links.push({ fullUrl, thumbUrl });
processedUrls.add(fullUrl); // Добавляем в множество обработанных
}
}
});
}); if (links.length === 0) {
log('Скриншоты не найдены в спойлерах, ищем по всему посту');
const aElements = postElement.querySelectorAll('a.zoom, a.postLink, a[href]');
aElements.forEach(link => {
if (!link.closest('div[style*="float"]') && !link.querySelector('img[style*="float"]')) {
const imgElement = link.querySelector('img');
const varElement = link.querySelector('var.postImg');
if (imgElement && imgElement.src) {
const fullUrl = varElement && varElement.getAttribute('title') ?
varElement.getAttribute('title').split('?')[0] :
link.href.split('?')[0];
const thumbUrl = imgElement.src.split('?')[0];
if (fullUrl.match(/\.(jpg|jpeg|png|webp|avif)$/i)) {
links.push({ fullUrl, thumbUrl });
}
}
}
});
} return links;
}, getCover: function(postElement) {
const varElement = postElement.querySelector('var.postImg.postImgAligned.img-right[title]');
if (varElement && varElement.getAttribute('title')) {
return varElement.getAttribute('title').split('?')[0];
} const alignedImg = postElement.querySelector('var.postImg.postImgAligned.img-right img');
if (alignedImg && alignedImg.src) {
return alignedImg.src.split('?')[0];
} return null;
}, openImage: function(imageUrl, fullImageUrl = null) {
const thumbnails = [];
const fullSizeUrls = [];
collectImagesFromPreview(thumbnails, fullSizeUrls); let currentIndex = thumbnails.indexOf(imageUrl);
if (currentIndex === -1) {
thumbnails.push(imageUrl);
fullSizeUrls.push(fullImageUrl || imageUrl);
currentIndex = thumbnails.length - 1;
} const useFullSize = settings.siteSettings.tormac.useFullSizeInLightbox;
if (useFullSize) {
processImageUrls(fullSizeUrls, function(processedUrls) {
showImageLightbox(imageUrl, thumbnails, processedUrls, currentIndex, true);
});
} else {
showImageLightbox(imageUrl, thumbnails, fullSizeUrls, currentIndex, useFullSize);
}
}
}
}; const sitesConfig = {
tormac: {
matchUrl: 'https://tormac.org/',
topicLinkSelector: 'a[href*="/torrent/"], a[href*="viewtopic.php?t="]',
firstPostSelector: 'div.post_body',
spoilerSelector: '.sp-wrap, .spoiler, .sp-body, .hide.spoiler-wrap',
getScreenshots: siteSpecificFunctions.tormac.getScreenshotLinks,
getScreenshotsFromPost: siteSpecificFunctions.tormac.getScreenshotsFromPost,
getCover: siteSpecificFunctions.tormac.getCover,
openImage: siteSpecificFunctions.tormac.openImage
}
}; //====================================
// ОБРАБОТКА ИЗОБРАЖЕНИЙ
//==================================== let cachedRequests = {}; function cleanupCache() {
const now = Date.now() / 1000;
Object.keys(cachedRequests).forEach(url => {
if (now - cachedRequests[url].timestamp > settings.cacheTTL) {
delete cachedRequests[url];
}
}); const cacheKeys = Object.keys(cachedRequests);
if (cacheKeys.length > settings.maxCacheSize) {
const keysToRemove = cacheKeys.slice(0, cacheKeys.length - settings.maxCacheSize);
keysToRemove.forEach(key => delete cachedRequests[key]);
}
} function processImageUrls(fullSizeUrls, callback) {
let pendingUrls = 0;
const processedUrls = [...fullSizeUrls]; if (fullSizeUrls.length === 0) {
return callback(processedUrls);
} for (let i = 0; i < fullSizeUrls.length; i++) {
const url = fullSizeUrls[i];
const supportedHostings = settings.siteSettings.tormac.supportedHostings;
const isUmpic = supportedHostings.includes('umpic') && url.match(/umpic\.ru\//);
const isFastPic = supportedHostings.includes('fastpic') && url.match(/fastpic\.(org|ru)\/(view|big)\//);
const isImageBam = supportedHostings.includes('imagebam') && url.match(/imagebam\.com\/view\//);
const isImgBox = supportedHostings.includes('imgbox') && url.match(/imgbox\.com\//);
const isImageBan = supportedHostings.includes('imageban') && url.match(/imageban\.ru\/show\//);
const isImgur = supportedHostings.includes('imgur') && url.match(/imgur\.com\/[a-zA-Z0-9]+/);
const isPostImage = supportedHostings.includes('postimage') && url.match(/postimg\.cc\/[a-zA-Z0-9]+/); if (isUmpic || isFastPic || isImageBam || isImgBox || isImageBan || isImgur || isPostImage) {
pendingUrls++;
let requestUrl = url; GM_xmlhttpRequest({
method: 'GET',
url: requestUrl,
onload: function(response) {
const html = response.responseText;
let directUrl = null; if (isUmpic) {
const imgMatch = html.match(/<img[^>]+src=["'](https?:\/\/s\d+\.umpic\.ru\/big\/[^"']+)["']/i);
if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
} else if (isFastPic) {
const imgMatch = html.match(/<img src="(https?:\/\/i\d+\.fastpic\.org\/big\/[^"]+)"[^>]*class="image/);
if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
} else if (isImageBam) {
const imgMatch = html.match(/<div class="view-image">.*?src="(https?:\/\/images\d+\.imagebam\.com\/[^"]+)".*?<\/div>/s);
if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
} else if (isImgBox) {
const imgMatch = html.match(/<div class="image-container">.*?src="(https?:\/\/images\d+\.imgbox\.com\/[^"]+)".*?<\/div>/s);
if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
} else if (isImageBan) {
const imgMatch = html.match(/<div class="docs-pictures clearfix">.*?src="(https?:\/\/i\d+\.imageban\.ru\/out\/[^"]+)".*?<\/div>/s);
if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
} else if (isImgur) {
const imgMatch = html.match(/<img[^>]*src="(https?:\/\/i\.imgur\.com\/[^"]+\.(jpg|png|gif))"/);
if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
} else if (isPostImage) {
const imgMatch = html.match(/<img[^>]*src="(https?:\/\/i\.postimg\.cc\/[^"]+\.(jpg|png|gif))"/);
if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
} if (directUrl) {
processedUrls[i] = directUrl;
log('Получена прямая ссылка:', directUrl);
} pendingUrls--;
if (pendingUrls === 0) {
callback(processedUrls);
}
},
onerror: function(error) {
log('Ошибка получения URL:', error);
pendingUrls--;
if (pendingUrls === 0) {
callback(processedUrls);
}
}
});
}
} if (pendingUrls === 0) {
callback(processedUrls);
}
} function collectImagesFromPreview(thumbnails, fullSizeUrls) {
const previewContainer = document.getElementById('torrent-preview');
if (previewContainer) {
const imageContainers = previewContainer.querySelectorAll('a[href]');
imageContainers.forEach(link => {
const img = link.querySelector('img');
if (img && img.src) {
thumbnails.push(img.src);
fullSizeUrls.push(link.href);
}
});
}
} //====================================
// ЛАЙТБОКС
//==================================== let isLightboxOpen = false; function createControlButton(content, title, onClick, fontSize = '28px') {
const button = createElement('div', {
innerHTML: content,
title: title
}, {
color: 'white',
fontSize: fontSize,
cursor: 'pointer',
opacity: '0.7',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
borderRadius: '50%',
width: `${settings.navButtonsSize}px`,
height: `${settings.navButtonsSize}px`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}); button.addEventListener('mouseenter', () => button.style.opacity = '1');
button.addEventListener('mouseleave', () => button.style.opacity = '0.7');
if (onClick) button.addEventListener('click', onClick);
return button;
} function showImageLightbox(imageUrl, thumbnails = [], fullSizeUrls = [], currentIndex = -1, useFullSizeForDisplay = false) {
isLightboxOpen = true;
const existingLightbox = document.getElementById('tormac-preview-lightbox');
if (existingLightbox) existingLightbox.remove(); const originalOverflow = document.body.style.overflow;
const isDarkTheme = settings.colorTheme === 'dark' ||
(settings.colorTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); const lightbox = createElement('div', { id: 'tormac-preview-lightbox' }, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: isDarkTheme ? 'rgba(0, 0, 0, 0.9)' : 'rgba(0, 0, 0, 0.8)',
zIndex: '10000',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer'
}); const imgContainer = createElement('div', {}, {
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
maxWidth: '95vw',
maxHeight: '95vh',
overflow: 'visible'
}); const contentContainer = createElement('div', {}, {
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'visible'
}); const loadingIndicator = createElement('div', {
textContent: 'Загрузка...'
}, {
color: 'white',
fontSize: '20px',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: '10001'
});
contentContainer.appendChild(loadingIndicator); const img = createElement('img', {}, {
maxWidth: `${settings.lightboxThumbnailSize}px`,
maxHeight: `${settings.lightboxThumbnailSize}px`,
border: `2px solid ${isDarkTheme ? '#444' : 'white'}`,
boxShadow: '0 0 20px rgba(0, 0, 0, 0.5)',
cursor: 'move',
display: 'none',
zIndex: '10002'
}); img.onload = () => {
loadingIndicator.style.display = 'none';
img.style.display = 'block';
}; img.onerror = () => {
loadingIndicator.textContent = 'Ошибка загрузки изображения';
}; img.src = useFullSizeForDisplay && currentIndex >= 0 && fullSizeUrls[currentIndex] ?
fullSizeUrls[currentIndex] : imageUrl; contentContainer.appendChild(img); const closeLightbox = () => {
document.body.style.overflow = originalOverflow;
lightbox.remove();
document.removeEventListener('keydown', keyHandler);
isLightboxOpen = false;
}; let isDragging = false;
let startX, startY, translateX = 0, translateY = 0, lastTranslateX = 0, lastTranslateY = 0;
let scale = 1;
const minScale = 0.5, maxScale = 3, scaleStep = 0.1; const prevImage = () => {
if (thumbnails.length > 1 && currentIndex > 0) {
currentIndex--;
loadingIndicator.style.display = 'block';
img.style.display = 'none';
img.src = useFullSizeForDisplay ? fullSizeUrls[currentIndex] : thumbnails[currentIndex];
updateNavButtons();
}
}; const nextImage = () => {
if (thumbnails.length > 1 && currentIndex < thumbnails.length - 1) {
currentIndex++;
loadingIndicator.style.display = 'block';
img.style.display = 'none';
img.src = useFullSizeForDisplay ? fullSizeUrls[currentIndex] : thumbnails[currentIndex];
updateNavButtons();
}
}; const resetImage = () => {
scale = 1;
translateX = 0;
translateY = 0;
lastTranslateX = 0;
lastTranslateY = 0;
contentContainer.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
}; const toggleFullscreen = () => {
if (!document.fullscreenElement) {
lightbox.requestFullscreen().catch(err => log('Ошибка включения полноэкранного режима:', err));
} else {
document.exitFullscreen().catch(err => log('Ошибка выхода из полноэкранного режима:', err));
}
}; const startDrag = e => {
if (e.button === 0) {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
lastTranslateX = translateX;
lastTranslateY = translateY;
contentContainer.style.cursor = 'grabbing';
e.preventDefault();
e.stopPropagation();
}
}; const moveDrag = e => {
if (!isDragging) return;
translateX = lastTranslateX + (e.clientX - startX);
translateY = lastTranslateY + (e.clientY - startY);
contentContainer.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
e.preventDefault();
e.stopPropagation();
}; const endDrag = e => {
if (isDragging) {
isDragging = false;
contentContainer.style.cursor = 'auto';
img.style.cursor = 'move';
if (e) e.stopPropagation();
}
}; let mouseStartedOnImage = false;
contentContainer.addEventListener('mousedown', () => mouseStartedOnImage = true); const keyHandler = e => {
if (e.key === settings.keyboardShortcuts.close) {
closeLightbox();
} else if (e.key === settings.keyboardShortcuts.prev) {
prevImage();
} else if (e.key === settings.keyboardShortcuts.next) {
nextImage();
} else if (e.key === settings.keyboardShortcuts.reset) {
resetImage();
} else if (e.key === settings.keyboardShortcuts.fullscreen) {
toggleFullscreen();
}
}; document.addEventListener('keydown', keyHandler); let prevButton = null, nextButton = null; const updateNavButtons = () => {
if (prevButton && nextButton) {
prevButton.style.visibility = currentIndex > 0 ? 'visible' : 'hidden';
nextButton.style.visibility = currentIndex < thumbnails.length - 1 ? 'visible' : 'hidden';
}
}; contentContainer.addEventListener('mouseenter', () => {
if (prevButton && nextButton && settings.navButtonsVisibility === 'hover') {
if (currentIndex > 0) prevButton.style.display = 'flex';
if (currentIndex < thumbnails.length - 1) nextButton.style.display = 'flex';
}
}); contentContainer.addEventListener('mouseleave', () => {
if (prevButton && nextButton && settings.navButtonsVisibility === 'hover') {
prevButton.style.display = 'none';
nextButton.style.display = 'none';
}
}); if (thumbnails.length > 1 && currentIndex !== -1) {
const buttonSize = settings.navButtonsSize;
const navButtonStyles = {
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
zIndex: '10005',
userSelect: 'none',
display: settings.navButtonsVisibility === 'always' ? 'flex' : 'none'
}; prevButton = createControlButton('‹', 'Предыдущее изображение', e => {
e.stopPropagation();
prevImage();
}, `${buttonSize}px`);
Object.assign(prevButton.style, navButtonStyles, { left: '-40px' }); nextButton = createControlButton('›', 'Следующее изображение', e => {
e.stopPropagation();
nextImage();
}, `${buttonSize}px`);
Object.assign(nextButton.style, navButtonStyles, { right: '-40px' }); if (settings.navButtonsVisibility === 'never') {
prevButton.style.display = 'none';
nextButton.style.display = 'none';
} contentContainer.appendChild(prevButton);
contentContainer.appendChild(nextButton);
updateNavButtons();
} contentContainer.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', moveDrag);
document.addEventListener('mouseup', endDrag); contentContainer.addEventListener('wheel', e => {
e.preventDefault();
e.stopPropagation();
const delta = e.deltaY > 0 ? -scaleStep : scaleStep;
scale = Math.min(maxScale, Math.max(minScale, scale + delta));
contentContainer.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
}, { passive: false }); lightbox.addEventListener('click', e => {
if (!mouseStartedOnImage) {
closeLightbox();
}
mouseStartedOnImage = false;
}); imgContainer.appendChild(contentContainer);
lightbox.appendChild(imgContainer);
document.body.appendChild(lightbox);
document.body.style.overflow = 'hidden';
} //====================================
// ПРЕДПРОСМОТР
//==================================== let previewWindow = null;
let currentPreviewLink = null;
let hoverPreviewLink = null;
let removeTimeout = null;
let currentRequest = null;
let requestInProgress = false;
const eventHandlers = []; function removePreviewWindow() {
if (previewWindow) {
previewWindow.remove();
previewWindow = null;
currentPreviewLink = null;
hoverPreviewLink = null;
}
eventHandlers.forEach(({ element, type, handler }) => {
element.removeEventListener(type, handler);
});
eventHandlers.length = 0;
} function handleSpoilersDynamic() {
const spoilerHeaders = document.querySelectorAll('.sp-head.clickable, .sp-wrap > div[onclick]');
spoilerHeaders.forEach(header => {
const clickHandler = () => {
setTimeout(() => {
const link = currentPreviewLink || hoverPreviewLink;
if (link) {
createPreviewWindow({ target: link }, sitesConfig.tormac);
}
}, 100);
};
header.addEventListener('click', clickHandler);
eventHandlers.push({ element: header, type: 'click', handler: clickHandler });
});
} function processResponseData(response, requestLink, siteConfig) {
if (!previewWindow || (currentPreviewLink !== requestLink && hoverPreviewLink !== requestLink)) return; const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
const firstPost = doc.querySelector(siteConfig.firstPostSelector); if (!firstPost) {
log('Ошибка: Не удалось найти первый пост с селектором', siteConfig.firstPostSelector);
previewWindow.innerHTML = 'Не удалось найти первый пост';
return;
} const siteName = 'tormac';
const coverUrl = siteConfig.getCover(firstPost);
const coverContainer = createElement('div', {}, {
float: 'right',
marginLeft: '10px',
marginBottom: '10px',
maxWidth: '150px'
}); if (coverUrl) {
const coverLink = createElement('a', { href: coverUrl });
const coverImage = createElement('img', {
src: coverUrl,
alt: 'Обложка'
}, {
maxWidth: '100%',
height: 'auto',
borderRadius: '6px'
}); addHoverEffect(coverLink, coverImage);
coverLink.addEventListener('click', e => {
e.preventDefault();
const clickBehavior = settings.siteSettings[siteName].clickBehavior;
if (clickBehavior === 'lightbox') {
siteConfig.openImage(coverUrl, coverUrl);
} else {
window.open(coverUrl, '_blank');
}
}); coverLink.appendChild(coverImage);
coverContainer.appendChild(coverLink);
} let screenshotLinks = siteConfig.getScreenshotsFromPost(firstPost); if (!previewWindow || (currentPreviewLink !== requestLink && hoverPreviewLink !== requestLink)) return; if (settings.hidePreviewIfEmpty && !coverUrl && screenshotLinks.length === 0) {
removePreviewWindow();
return;
} const isDarkTheme = settings.colorTheme === 'dark' ||
(settings.colorTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); previewWindow.innerHTML = ''; if (coverUrl) {
previewWindow.appendChild(coverContainer);
} const infoElement = createElement('div', {
textContent: `Скриншоты: ${screenshotLinks.length ? screenshotLinks.length : 'Не найдены'}`
}, {
marginBottom: '10px'
});
previewWindow.appendChild(infoElement); if (screenshotLinks.length > 0) {
const imagesContainer = createElement('div', {}, {
display: 'grid',
gridTemplateColumns: `repeat(${settings.previewGridColumns}, 1fr)`,
gap: '5px',
justifyItems: 'center'
}); const maxVisible = settings.neverUseSpoilers ? screenshotLinks.length : settings.maxThumbnailsBeforeSpoiler;
addImagesToContainer(imagesContainer, screenshotLinks, siteConfig.openImage, siteName, 0, maxVisible);
previewWindow.appendChild(imagesContainer); if (!settings.neverUseSpoilers && screenshotLinks.length > settings.maxThumbnailsBeforeSpoiler) {
const spoilerContainer = createElement('div', {}, {
marginTop: '10px'
}); const spoilerButton = createElement('button', {
textContent: 'Показать остальные скриншоты'
}, {
background: isDarkTheme ? '#333' : '#f0f0f0',
border: `1px solid ${isDarkTheme ? '#555' : '#ccc'}`,
color: isDarkTheme ? '#eee' : 'black',
padding: '5px 10px',
cursor: 'pointer',
width: '100%',
borderRadius: '4px'
}); const hiddenImagesContainer = createElement('div', {}, {
display: 'none',
gridTemplateColumns: `repeat(${settings.previewGridColumns}, 1fr)`,
gap: '5px',
justifyItems: 'center',
marginTop: '10px'
}); addImagesToContainer(hiddenImagesContainer, screenshotLinks, siteConfig.openImage, siteName, settings.maxThumbnailsBeforeSpoiler); const buttonClickHandler = () => {
hiddenImagesContainer.style.display = hiddenImagesContainer.style.display === 'none' ? 'grid' : 'none';
spoilerButton.textContent = hiddenImagesContainer.style.display === 'none' ?
'Показать остальные скриншоты' : 'Скрыть скриншоты';
};
spoilerButton.addEventListener('click', buttonClickHandler);
eventHandlers.push({ element: spoilerButton, type: 'click', handler: buttonClickHandler }); spoilerContainer.appendChild(spoilerButton);
spoilerContainer.appendChild(hiddenImagesContainer);
previewWindow.appendChild(spoilerContainer);
}
} if (previewWindow) {
const mouseEnterHandler = () => {
clearTimeout(removeTimeout);
removeTimeout = null;
}; const mouseLeaveHandler = () => {
clearTimeout(removeTimeout);
if (!isLightboxOpen) {
removeTimeout = setTimeout(() => removePreviewWindow(), settings.previewHideDelay);
}
}; previewWindow.addEventListener('mouseenter', mouseEnterHandler);
previewWindow.addEventListener('mouseleave', mouseLeaveHandler);
requestLink.addEventListener('mouseleave', mouseLeaveHandler); eventHandlers.push(
{ element: previewWindow, type: 'mouseenter', handler: mouseEnterHandler },
{ element: previewWindow, type: 'mouseleave', handler: mouseLeaveHandler },
{ element: requestLink, type: 'mouseleave', handler: mouseLeaveHandler }
);
}
} function createPreviewWindow(event, siteConfig) {
if (!settings.enableAutoPreview || !settings.siteSettings.tormac.enabled) return; const link = event.target.closest(siteConfig.topicLinkSelector);
if (!link || link.closest('h1.maintitle')) return; hoverPreviewLink = link;
if (removeTimeout) {
clearTimeout(removeTimeout);
removeTimeout = null;
} if (previewWindow && currentPreviewLink === link) return; if (currentRequest && requestInProgress) {
try {
currentRequest.abort();
} catch (e) {
log('Ошибка при отмене запроса:', e);
}
currentRequest = null;
requestInProgress = false;
} removePreviewWindow();
currentPreviewLink = link; const isDarkTheme = settings.colorTheme === 'dark' ||
(settings.colorTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); previewWindow = createElement('div', {
id: 'torrent-preview',
innerHTML: 'Загрузка...'
}, {
position: 'absolute',
backgroundColor: isDarkTheme ? '#222' : 'white',
color: isDarkTheme ? '#eee' : 'black',
border: `1px solid ${isDarkTheme ? '#444' : '#ccc'}`,
padding: '10px',
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
zIndex: '1000',
maxWidth: `${settings.previewMaxWidth}px`,
maxHeight: `${settings.previewMaxHeight}px`,
overflowY: 'auto',
borderRadius: '8px'
}); document.body.appendChild(previewWindow); const updatePosition = () => {
if (!previewWindow) return;
const rect = link.getBoundingClientRect();
const scrollX = window.scrollX, scrollY = window.scrollY;
switch (settings.previewPosition) {
case 'topLeft':
previewWindow.style.top = `${rect.top + scrollY - previewWindow.offsetHeight - 5}px`;
previewWindow.style.left = `${rect.left + scrollX}px`;
break;
case 'topRight':
previewWindow.style.top = `${rect.top + scrollY - previewWindow.offsetHeight - 5}px`;
previewWindow.style.left = `${rect.right + scrollX - previewWindow.offsetWidth}px`;
break;
case 'bottomLeft':
previewWindow.style.top = `${rect.bottom + scrollY + 5}px`;
previewWindow.style.left = `${rect.left + scrollX}px`;
break;
case 'bottomRight':
default:
previewWindow.style.top = `${rect.bottom + scrollY + 5}px`;
previewWindow.style.left = `${rect.right + scrollX - previewWindow.offsetWidth}px`;
break;
}
}; updatePosition();
const scrollHandler = () => updatePosition();
window.addEventListener('scroll', scrollHandler);
eventHandlers.push({ element: window, type: 'scroll', handler: scrollHandler }); if (window.location.href.includes('viewtopic.php?t=')) {
const firstPost = document.querySelector(siteConfig.firstPostSelector);
if (firstPost) {
processResponseData({ responseText: document.documentElement.outerHTML }, link, siteConfig);
handleSpoilersDynamic();
return;
}
} let requestUrl = link.href;
if (requestUrl.includes('/torrent/')) {
const topicId = requestUrl.match(/\.(\d+)$/);
if (!topicId) {
log('Ошибка: Не удалось извлечь topicId из URL:', requestUrl);
previewWindow.innerHTML = 'Ошибка обработки URL';
return;
}
requestUrl = `https://tormac.org/viewtopic.php?t=${topicId[1]}`;
} if (!requestUrl.startsWith('https://tormac.org/')) {
log('Ошибка: Недопустимый URL:', requestUrl);
previewWindow.innerHTML = 'Недопустимый URL';
return;
} const cached = cachedRequests[requestUrl];
if (cached && (Date.now() / 1000 - cached.timestamp) < settings.cacheTTL) {
log('Используем кэшированный ответ для:', requestUrl);
processResponseData(cached, link, siteConfig);
handleSpoilersDynamic();
return;
} requestInProgress = true;
currentRequest = GM_xmlhttpRequest({
method: 'GET',
url: requestUrl,
onload: response => {
requestInProgress = false;
cachedRequests[requestUrl] = { responseText: response.responseText, timestamp: Date.now() / 1000 };
cleanupCache(); if (hoverPreviewLink !== link && currentPreviewLink !== link) {
log('Мышь перешла на другую ссылку, игнорируем ответ для:', requestUrl);
return;
} if (!previewWindow) return;
processResponseData(response, link, siteConfig);
handleSpoilersDynamic();
currentRequest = null;
},
onerror: error => {
requestInProgress = false;
currentRequest = null;
if (previewWindow) previewWindow.innerHTML = 'Ошибка загрузки данных';
log('Ошибка AJAX-запроса:', error);
}
});
} const handleMouseEnter = debounce(function(e) {
if (isLightboxOpen) return;
const siteConfig = sitesConfig.tormac;
if (siteConfig && window.location.href.startsWith(siteConfig.matchUrl)) {
createPreviewWindow(e, siteConfig);
}
}, 100); const handleMouseOver = e => {
if (isLightboxOpen) return;
const link = e.target.closest(sitesConfig.tormac.topicLinkSelector);
if (link && link !== hoverPreviewLink) {
hoverPreviewLink = link;
}
}; //====================================
// ИНИЦИАЛИЗАЦИЯ
//==================================== document.addEventListener('DOMContentLoaded', () => {
handleSpoilersDynamic(); const observer = new MutationObserver(() => {
const link = currentPreviewLink || hoverPreviewLink;
if (link) {
createPreviewWindow({ target: link }, sitesConfig.tormac);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}); GM_registerMenuCommand('⚙️ Настройки tormac Preview', openSettingsDialog); document.addEventListener('mouseenter', handleMouseEnter, true);
document.addEventListener('mouseover', handleMouseOver);
})();
|
|
Yello
|
admin, удобные штуки
|
|
|