// ==UserScript==
// @name nCore – Tools
// @namespace https://github.com/lkristof/userscripts
// @version 1.2.4
// @description nCore segédscript: qBittorrent integráció, lista/poszter torrentnézet, linktisztítás, reklám- és köszönetrejtés, képbeágyazás, látott filmek és torrentkiemelés.
// @icon https://static.ncore.pro/styles/ncore.ico
//
// @match https://ncore.pro/*
//
// @homepageURL https://github.com/lkristof/userscripts
// @supportURL https://github.com/lkristof/userscripts/issues
// @downloadURL https://raw.githubusercontent.com/lkristof/userscripts/main/ncore/ncore-tools.user.js
// @updateURL https://raw.githubusercontent.com/lkristof/userscripts/main/ncore/ncore-tools.user.js
//
// @grant GM_xmlhttpRequest
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant unsafeWindow
//
// @connect api.github.com
// @connect gist.githubusercontent.com
// @connect *
//
// @run-at document-idle
// ==/UserScript==
(async function () {
'use strict';
// -------------------------------------------------------------------------
// Storage
// -------------------------------------------------------------------------
const LS_PREFIX = 'ncore_';
const GM_KEY_QB_URL = 'qb_url';
const GM_KEY_SETTINGS = 'tools_settings';
const GM_KEY_SEEN_SYNC = 'seen_sync_config';
const GM_KEY_TORRENT_VIEW = 'torrent_view';
const LS_SEEN_SYNC_STATE = LS_PREFIX + 'seen_sync_state';
const LS_DOWNLOAD_SYNC_STATE = LS_PREFIX + 'download_sync_state';
const DEFAULT_GIST_FILENAME = 'ncore_seen.json';
const DEFAULT_SETTINGS = {
qbittorrent: true,
highlight: true,
highlightMinPlus: 3,
seen: true,
embedImages: true,
removeAds: true,
noThanks: true,
dedereferer: true,
};
function normalizeHighlightMinPlus(value) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed >= 1 ? parsed : DEFAULT_SETTINGS.highlightMinPlus;
}
function getHighlightMinPlus() {
return normalizeHighlightMinPlus(settings?.highlightMinPlus);
}
async function gmGet(key, def = '') {
try {
if (typeof GM_getValue === 'function') return GM_getValue(key, def);
if (typeof GM !== 'undefined' && typeof GM.getValue === 'function') return await GM.getValue(key, def);
} catch (_) {}
try {
const v = localStorage.getItem(LS_PREFIX + key);
return v === null ? def : v;
} catch (_) {
return def;
}
}
async function gmSet(key, value) {
try {
if (typeof GM_setValue === 'function') return GM_setValue(key, value);
if (typeof GM !== 'undefined' && typeof GM.setValue === 'function') return await GM.setValue(key, value);
} catch (_) {}
try {
localStorage.setItem(LS_PREFIX + key, String(value));
} catch (_) {}
}
async function gmDel(key) {
try {
if (typeof GM_deleteValue === 'function') return GM_deleteValue(key);
if (typeof GM !== 'undefined' && typeof GM.deleteValue === 'function') return await GM.deleteValue(key);
} catch (_) {}
try {
localStorage.removeItem(LS_PREFIX + key);
} catch (_) {}
}
async function loadSettings() {
const raw = await gmGet(GM_KEY_SETTINGS, '');
if (!raw) return { ...DEFAULT_SETTINGS };
try {
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
return { ...DEFAULT_SETTINGS, ...(parsed || {}) };
} catch (_) {
return { ...DEFAULT_SETTINGS };
}
}
async function saveSettings(settings) {
await gmSet(GM_KEY_SETTINGS, JSON.stringify(settings));
}
async function loadSeenSyncConfig() {
const raw = await gmGet(GM_KEY_SEEN_SYNC, '');
if (!raw) return { gistToken: '', gistId: '', gistFilename: DEFAULT_GIST_FILENAME };
try {
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
return {
gistToken: String(parsed?.gistToken || ''),
gistId: String(parsed?.gistId || ''),
gistFilename: String(parsed?.gistFilename || DEFAULT_GIST_FILENAME),
};
} catch (_) {
return { gistToken: '', gistId: '', gistFilename: DEFAULT_GIST_FILENAME };
}
}
async function saveSeenSyncConfig(config) {
await gmSet(GM_KEY_SEEN_SYNC, JSON.stringify({
gistToken: String(config?.gistToken || '').trim(),
gistId: String(config?.gistId || '').trim(),
gistFilename: String(config?.gistFilename || DEFAULT_GIST_FILENAME).trim() || DEFAULT_GIST_FILENAME,
}));
}
const settings = await loadSettings();
const $ = (sel, root = document) => root.querySelector(sel);
// -------------------------------------------------------------------------
// Toast UI
// -------------------------------------------------------------------------
let toastContainer = null;
function getToastContainer() {
if (!toastContainer) {
toastContainer = document.createElement('div');
Object.assign(toastContainer.style, {
position: 'fixed',
bottom: '20px',
right: '20px',
display: 'flex',
flexDirection: 'column',
gap: '8px',
zIndex: '100001',
fontFamily: 'Verdana, Geneva, Arial, Helvetica, sans-serif',
});
document.body.appendChild(toastContainer);
}
return toastContainer;
}
function showToast(message, type = 'success', duration = 3000) {
const container = getToastContainer();
const toast = document.createElement('div');
const accent = type === 'success' ? '#84bd00' : '#b94a48';
Object.assign(toast.style, {
position: 'relative',
minWidth: '240px',
maxWidth: '360px',
overflow: 'hidden',
padding: '10px 12px 12px 14px',
boxSizing: 'border-box',
border: '1px solid #35363a',
borderLeft: `3px solid ${accent}`,
borderRadius: '4px',
background: '#222326',
boxShadow: '0 5px 18px rgba(0, 0, 0, .48)',
color: '#cbCDD0',
fontSize: '10px',
fontWeight: 'normal',
lineHeight: '15px',
opacity: '0',
transform: 'translateX(14px)',
transition: 'opacity .18s ease, transform .18s ease',
});
const text = document.createElement('div');
text.textContent = message;
const progressTrack = document.createElement('div');
Object.assign(progressTrack.style, {
position: 'absolute',
left: '0',
right: '0',
bottom: '0',
height: '3px',
background: '#2e2f33',
});
const progress = document.createElement('div');
Object.assign(progress.style, {
width: '100%',
height: '100%',
background: accent,
transformOrigin: 'left center',
transform: 'scaleX(1)',
});
progressTrack.appendChild(progress);
toast.append(text, progressTrack);
container.appendChild(toast);
requestAnimationFrame(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateX(0)';
requestAnimationFrame(() => {
progress.style.transition = `transform ${Math.max(0, duration)}ms linear`;
progress.style.transform = 'scaleX(0)';
});
});
if (container.children.length > 5) container.removeChild(container.firstChild);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(14px)';
setTimeout(() => toast.remove(), 180);
}, duration);
}
// -------------------------------------------------------------------------
// „Láttam már” Gist szinkron
// -------------------------------------------------------------------------
function createSeenSync() {
let pushTimer = null;
let syncing = null;
function safeJsonParse(raw, fallback) {
try { return JSON.parse(raw); } catch (_) { return fallback; }
}
function loadLocalState() {
const parsed = safeJsonParse(localStorage.getItem(LS_SEEN_SYNC_STATE) || '{}', {});
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
}
function saveLocalState(state) {
localStorage.setItem(LS_SEEN_SYNC_STATE, JSON.stringify(state || {}));
}
function loadDownloadState() {
const parsed = safeJsonParse(localStorage.getItem(LS_DOWNLOAD_SYNC_STATE) || '{}', {});
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
}
function saveDownloadState(state) {
localStorage.setItem(LS_DOWNLOAD_SYNC_STATE, JSON.stringify(state || {}));
}
function normalizeEntry(entry) {
if (!entry || typeof entry !== 'object') return null;
const ts = Number(entry.ts) || 0;
const title = String(entry.title || '').replace(/\s+/g, ' ').trim();
return {
seen: Boolean(entry.seen),
ts,
...(title ? { title } : {}),
};
}
function normalizeState(state) {
const out = {};
for (const [imdbId, entry] of Object.entries(state || {})) {
if (!/^\d+$/.test(imdbId)) continue;
const normalized = normalizeEntry(entry);
if (normalized) out[imdbId] = normalized;
}
return out;
}
function normalizeDownloadEntry(entry) {
if (!entry || typeof entry !== 'object') return null;
const ts = Number(entry.ts) || 0;
const title = String(entry.title || '').replace(/\s+/g, ' ').trim();
return {
ts,
...(title ? { title } : {}),
};
}
function normalizeDownloads(state) {
const out = {};
for (const [torrentId, entry] of Object.entries(state || {})) {
if (!/^\d+$/.test(torrentId)) continue;
const normalized = normalizeDownloadEntry(entry);
if (normalized) out[torrentId] = normalized;
}
return out;
}
function migrateLegacySeenKeys() {
const state = normalizeState(loadLocalState());
let changed = false;
// A régi verzió az IMDb számszerű azonosítóját közvetlenül localStorage kulcsként használta.
// Csak IMDb-re jellemző 7–9 számjegyes kulcsokat veszünk át, hogy más helyi adatot ne érintsünk.
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!/^\d{7,9}$/.test(key || '') || localStorage.getItem(key) === null) continue;
if (!state[key]) {
state[key] = { seen: true, ts: 1 };
changed = true;
}
}
if (changed) saveLocalState(state);
return state;
}
function compareEntries(a, b) {
const aa = normalizeEntry(a) || { seen: false, ts: 0 };
const bb = normalizeEntry(b) || { seen: false, ts: 0 };
if (aa.ts !== bb.ts) return aa.ts - bb.ts;
if (aa.seen === bb.seen) return 0;
// Azonos timestampnél a törlés nyerjen, így nem támad fel egy visszavont jelölés.
return aa.seen ? -1 : 1;
}
function mergeStates(remoteState, localState) {
const remote = normalizeState(remoteState);
const local = normalizeState(localState);
const merged = {};
for (const id of new Set([...Object.keys(remote), ...Object.keys(local)])) {
const r = remote[id];
const l = local[id];
if (!r) merged[id] = l;
else if (!l) merged[id] = r;
else merged[id] = compareEntries(l, r) >= 0 ? l : r;
}
return merged;
}
function mergeDownloads(remoteState, localState) {
const remote = normalizeDownloads(remoteState);
const local = normalizeDownloads(localState);
const merged = {};
for (const id of new Set([...Object.keys(remote), ...Object.keys(local)])) {
const r = remote[id];
const l = local[id];
if (!r) merged[id] = l;
else if (!l) merged[id] = r;
else merged[id] = (Number(l.ts) || 0) >= (Number(r.ts) || 0) ? l : r;
}
return merged;
}
function applyState(state) {
const normalized = normalizeState(state);
saveLocalState(normalized);
for (const [imdbId, entry] of Object.entries(normalized)) {
if (entry.seen) localStorage.setItem(imdbId, '1');
else localStorage.removeItem(imdbId);
}
return normalized;
}
function applyDownloads(state) {
const normalized = normalizeDownloads(state);
saveDownloadState(normalized);
return normalized;
}
function sameState(a, b) {
return JSON.stringify(normalizeState(a)) === JSON.stringify(normalizeState(b));
}
function sameDownloads(a, b) {
return JSON.stringify(normalizeDownloads(a)) === JSON.stringify(normalizeDownloads(b));
}
async function getConfig() {
const config = await loadSeenSyncConfig();
return {
gistToken: config.gistToken.trim(),
gistId: config.gistId.trim(),
gistFilename: (config.gistFilename || DEFAULT_GIST_FILENAME).trim() || DEFAULT_GIST_FILENAME,
};
}
async function getEnabledConfig() {
const config = await getConfig();
if (!config.gistToken || !config.gistId || !config.gistFilename) return null;
return config;
}
function httpRequest({ method = 'GET', url, headers = {}, body = null }) {
if (typeof GM_xmlhttpRequest === 'function') {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method,
url,
headers,
data: body,
responseType: 'text',
timeout: 20000,
onload: resolve,
onerror: () => reject(new Error(`Network error: ${method} ${url}`)),
ontimeout: () => reject(new Error(`Request timeout: ${method} ${url}`)),
onabort: () => reject(new Error(`Request aborted: ${method} ${url}`)),
});
});
}
return fetch(url, { method, headers, body }).then(async response => ({
status: response.status,
statusText: response.statusText,
responseText: await response.text(),
}));
}
function githubHeaders(config, jsonBody = false) {
return {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${config.gistToken}`,
'X-GitHub-Api-Version': '2022-11-28',
...(jsonBody ? { 'Content-Type': 'application/json' } : {}),
};
}
async function fetchRemote(config) {
const response = await httpRequest({
method: 'GET',
url: `https://api.github.com/gists/${encodeURIComponent(config.gistId)}`,
headers: githubHeaders(config),
});
if (response.status < 200 || response.status >= 300) {
throw new Error(`Gist lekérés sikertelen: HTTP ${response.status}`);
}
const gist = safeJsonParse(response.responseText, null);
if (!gist || typeof gist !== 'object') throw new Error('Érvénytelen GitHub válasz.');
const file = gist.files?.[config.gistFilename];
if (!file) return { movies: {}, downloads: {} };
let content = typeof file.content === 'string' ? file.content : '';
if ((file.truncated || !content) && file.raw_url) {
const raw = await httpRequest({
method: 'GET',
url: file.raw_url,
headers: { Authorization: `Bearer ${config.gistToken}` },
});
if (raw.status < 200 || raw.status >= 300) {
throw new Error(`Gist raw lekérés sikertelen: HTTP ${raw.status}`);
}
content = raw.responseText || '';
}
if (!content.trim()) return { movies: {}, downloads: {} };
const blob = safeJsonParse(content, null);
if (!blob || typeof blob !== 'object' || Array.isArray(blob)) {
throw new Error(`A Gist fájl nem érvényes JSON: ${config.gistFilename}`);
}
return {
movies: normalizeState(blob.movies || {}),
downloads: normalizeDownloads(blob.downloads || {}),
};
}
function serializeObjectEntries(entries) {
return entries.map(([id, entry]) => {
const entryJson = JSON.stringify(entry, null, 2)
.split('\n')
.map((line, index) => index === 0 ? line : ' ' + line)
.join('\n');
return ` ${JSON.stringify(id)}: ${entryJson}`;
}).join(',\n');
}
function serializeGistState(moviesState, downloadsState) {
const moviesEntries = Object.entries(normalizeState(moviesState))
.sort(([, a], [, b]) => (Number(b?.ts) || 0) - (Number(a?.ts) || 0));
const downloadEntries = Object.entries(normalizeDownloads(downloadsState))
.sort(([, a], [, b]) => (Number(b?.ts) || 0) - (Number(a?.ts) || 0));
const moviesJson = serializeObjectEntries(moviesEntries);
const downloadsJson = serializeObjectEntries(downloadEntries);
return [
'{',
' "version": 1,',
' "movies": {',
moviesJson,
' },',
' "downloads": {',
downloadsJson,
' }',
'}',
].filter((line, index) => line || (index !== 3 && index !== 6)).join('\n');
}
async function pushRemote(config, moviesState, downloadsState) {
const body = {
files: {
[config.gistFilename]: {
// Kézzel sorosítjuk, mert a JSON.stringify a csak számjegyekből álló
// objektumkulcsokat numerikusan rendezi, függetlenül a beszúrási sorrendtől.
content: serializeGistState(moviesState, downloadsState),
},
},
};
const response = await httpRequest({
method: 'PATCH',
url: `https://api.github.com/gists/${encodeURIComponent(config.gistId)}`,
headers: githubHeaders(config, true),
body: JSON.stringify(body),
});
if (response.status < 200 || response.status >= 300) {
throw new Error(`Gist frissítés sikertelen: HTTP ${response.status}`);
}
}
async function doSync(forcePush = false) {
const config = await getEnabledConfig();
if (!config) return { ok: false, disabled: true };
// Egy időben csak egy hálózati szinkron futhat.
if (syncing) return syncing;
syncing = (async () => {
const localMovies = migrateLegacySeenKeys();
const localDownloads = normalizeDownloads(loadDownloadState());
const remote = await fetchRemote(config);
const mergedMovies = mergeStates(remote.movies, localMovies);
const mergedDownloads = mergeDownloads(remote.downloads, localDownloads);
applyState(mergedMovies);
applyDownloads(mergedDownloads);
const changed = !sameState(remote.movies, mergedMovies)
|| !sameDownloads(remote.downloads, mergedDownloads);
if (changed) {
// Push előtt még egyszer olvasunk, hogy a közben más gépről érkezett változás is bekerüljön.
const latestRemote = await fetchRemote(config);
const latestMovies = mergeStates(latestRemote.movies, loadLocalState());
const latestDownloads = mergeDownloads(latestRemote.downloads, loadDownloadState());
applyState(latestMovies);
applyDownloads(latestDownloads);
const latestChanged = !sameState(latestRemote.movies, latestMovies)
|| !sameDownloads(latestRemote.downloads, latestDownloads);
if (latestChanged || forcePush) {
await pushRemote(config, latestMovies, latestDownloads);
}
} else if (forcePush) {
// A kézi szinkron a tartalmilag változatlan Gistet is újraírja,
// így a movies és downloads objektumok is ts DESC sorrendben maradnak.
await pushRemote(config, mergedMovies, mergedDownloads);
}
return {
ok: true,
count: Object.values(mergedMovies).filter(entry => entry.seen).length,
downloadCount: Object.keys(mergedDownloads).length,
};
})();
try {
return await syncing;
} finally {
syncing = null;
}
}
function scheduleSync() {
if (pushTimer) clearTimeout(pushTimer);
pushTimer = setTimeout(() => {
pushTimer = null;
doSync().catch(error => console.warn('[nCore Tools] Seen Gist sync failed:', error));
}, 1500);
}
async function init() {
migrateLegacySeenKeys();
try {
await doSync();
} catch (error) {
console.warn('[nCore Tools] Seen Gist sync init failed:', error);
}
}
function setSeen(imdbId, seen, title = '') {
const state = normalizeState(loadLocalState());
const previousTitle = state[imdbId]?.title || '';
const normalizedTitle = String(title || previousTitle).replace(/\s+/g, ' ').trim();
state[imdbId] = {
seen: Boolean(seen),
ts: Date.now(),
...(normalizedTitle ? { title: normalizedTitle } : {}),
};
applyState(state);
scheduleSync();
}
function isSeen(imdbId) {
return localStorage.getItem(imdbId) !== null;
}
async function syncNow() {
if (pushTimer) {
clearTimeout(pushTimer);
pushTimer = null;
}
return doSync(true);
}
function getSeenMovies() {
return Object.entries(normalizeState(loadLocalState()))
.filter(([, entry]) => entry.seen)
.sort(([, a], [, b]) => (Number(b?.ts) || 0) - (Number(a?.ts) || 0))
.map(([imdbId, entry]) => ({ imdbId, ...entry }));
}
function addDownload(torrentId, title = '') {
const id = String(torrentId || '').trim();
if (!/^\d+$/.test(id)) return;
const state = normalizeDownloads(loadDownloadState());
const previousTitle = state[id]?.title || '';
const normalizedTitle = String(title || previousTitle).replace(/\s+/g, ' ').trim();
state[id] = {
ts: Date.now(),
...(normalizedTitle ? { title: normalizedTitle } : {}),
};
applyDownloads(state);
scheduleSync();
}
function getDownloads() {
return Object.entries(normalizeDownloads(loadDownloadState()))
.sort(([, a], [, b]) => (Number(b?.ts) || 0) - (Number(a?.ts) || 0))
.map(([torrentId, entry]) => ({ torrentId, ...entry }));
}
return { init, isSeen, setSeen, syncNow, getSeenMovies, addDownload, getDownloads };
}
const seenSync = createSeenSync();
// -------------------------------------------------------------------------
// qBittorrent URL
// -------------------------------------------------------------------------
function normalizeBaseUrl(u) {
if (!u) return '';
u = String(u).trim();
if (u && !/^https?:\/\//i.test(u)) u = 'http://' + u;
return u.replace(/\/+$/, '');
}
async function getQBUrl() {
return normalizeBaseUrl(await gmGet(GM_KEY_QB_URL, ''));
}
async function setQBUrl(u) {
const norm = normalizeBaseUrl(u);
if (norm) await gmSet(GM_KEY_QB_URL, norm);
else await gmDel(GM_KEY_QB_URL);
return norm;
}
async function promptForQBUrl() {
const current = await getQBUrl();
const input = window.prompt(
'qBittorrent WebUI URL (pl. http://127.0.0.1:8080)',
current || 'http://127.0.0.1:8080'
);
if (input === null) return null;
const saved = await setQBUrl(input);
showToast(saved ? 'qBittorrent URL elmentve: ' + saved : 'qBittorrent URL törölve.', saved ? 'success' : 'error');
await refreshInfosavQBLinkState();
return saved;
}
async function sendToQB(dlUrl, downloadInfo = {}) {
let qb = await getQBUrl();
if (!qb) {
showToast('Nincs beállítva qBittorrent URL. Beállítás…', 'error');
qb = await promptForQBUrl();
if (!qb) return;
}
if (typeof GM_xmlhttpRequest !== 'function') {
showToast('GM_xmlhttpRequest nem elérhető ebben a userscript környezetben.', 'error', 5000);
return;
}
GM_xmlhttpRequest({
method: 'POST',
url: qb + '/api/v2/torrents/add',
data: 'urls=' + encodeURIComponent(dlUrl),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
onload(resp) {
if (resp.status === 200) {
const torrentId = String(downloadInfo.torrentId || new URL(dlUrl).searchParams.get('id') || '');
seenSync.addDownload(torrentId, downloadInfo.title || '');
showToast('Torrent elküldve qBittorrentnek!');
} else {
showToast('Sikertelen: HTTP ' + resp.status, 'error');
}
},
onerror() {
showToast('Hiba a qBittorrent felé küldés közben.', 'error');
},
});
}
// -------------------------------------------------------------------------
// Beállítások modal
// -------------------------------------------------------------------------
const SETTING_GROUPS = [
{
title: 'Integrációk',
settings: [
['qbittorrent', 'qBittorrent integráció'],
],
},
{
title: 'Torrentlista',
settings: [
['highlight', 'Népszerű torrentek kiemelése'],
['seen', '„Láttam már” jelölés dupla kattintással'],
['embedImages', 'Képek beágyazása a torrentlistán'],
],
},
{
title: 'Oldal tisztítása',
settings: [
['removeAds', 'Reklámok eltávolítása'],
['noThanks', 'Köszönetek elrejtése'],
['dedereferer', 'Dereferer linkek eltávolítása'],
],
},
];
let settingsKeydownHandler = null;
function closeSettingsModal() {
document.getElementById('ncore-tools-settings-overlay')?.remove();
if (settingsKeydownHandler) {
document.removeEventListener('keydown', settingsKeydownHandler);
settingsKeydownHandler = null;
}
}
function ensureSettingsModalStyle() {
if (document.getElementById('ncore-tools-settings-style')) return;
const style = document.createElement('style');
style.id = 'ncore-tools-settings-style';
style.textContent = `
#ncore-tools-settings-overlay {
position: fixed;
inset: 0;
z-index: 100000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(12, 13, 14, 0.78);
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
font-size: 10px;
color: #adafb2;
}
#ncore-tools-settings-panel {
width: min(500px, 94vw);
overflow: hidden;
background: #222326;
border: 1px solid #35363a;
border-top: 2px solid #84bd00;
border-radius: 3px;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55);
}
#ncore-tools-settings-header {
position: relative;
padding: 12px 14px 0;
background: #2e2f33;
border-bottom: 1px solid #333437;
}
#ncore-tools-settings-title {
margin: 0;
padding: 0 28px 0 0;
color: #cbCDD0;
font-size: 14px;
line-height: 18px;
font-weight: normal;
}
#ncore-tools-settings-subtitle {
margin-top: 4px;
color: #777b80;
font-size: 9px;
line-height: 13px;
}
#ncore-tools-settings-close {
position: absolute;
top: 8px;
right: 9px;
width: 24px;
height: 24px;
padding: 0;
border: 1px solid transparent;
border-radius: 3px;
background: transparent;
color: #777b80;
font: bold 17px/20px Arial, sans-serif;
cursor: pointer;
}
#ncore-tools-settings-close:hover {
border-color: #43454c;
background: #34363a;
color: #cbCDD0;
}
#ncore-tools-settings-tabs {
display: flex;
gap: 2px;
margin: 10px -14px 0;
padding: 0 10px;
border-top: 1px solid #38393d;
background: #292a2e;
}
.ncore-tools-settings-tab {
position: relative;
min-height: 31px;
padding: 6px 10px;
border: 0;
background: transparent;
color: #777b80;
font: bold 10px Verdana, Geneva, Arial, Helvetica, sans-serif;
cursor: pointer;
}
.ncore-tools-settings-tab:hover,
.ncore-tools-settings-tab:focus-visible {
color: #cbCDD0;
outline: none;
}
.ncore-tools-settings-tab[aria-selected="true"] {
color: #cbCDD0;
background: #222326;
}
.ncore-tools-settings-tab[aria-selected="true"]::after {
content: '';
position: absolute;
left: 8px;
right: 8px;
bottom: 0;
height: 2px;
background: #84bd00;
}
#ncore-tools-settings-form {
padding: 8px 10px;
background: #222326;
}
.ncore-tools-settings-tab-panel[hidden] { display: none; }
.ncore-tools-setting-group {
margin-top: 9px;
overflow: hidden;
border: 1px solid #303135;
border-radius: 3px;
background: #1e1f22;
}
.ncore-tools-setting-group:first-child { margin-top: 0; }
.ncore-tools-setting-group-title {
padding: 6px 8px;
border-bottom: 1px solid #303135;
background: #292a2e;
color: #84bd00;
font-size: 9px;
font-weight: bold;
line-height: 13px;
}
.ncore-tools-setting-row {
display: flex;
align-items: center;
min-height: 31px;
margin: 0;
padding: 5px 8px;
border-top: 1px solid #303135;
color: #adafb2;
cursor: pointer;
user-select: none;
box-sizing: border-box;
}
.ncore-tools-setting-group .ncore-tools-setting-row:first-of-type { border-top: 0; }
.ncore-tools-setting-row:hover {
background: #2a2b2f;
color: #cbCDD0;
}
.ncore-tools-setting-row input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 14px;
height: 14px;
flex: 0 0 14px;
margin: 0 9px 0 0;
border: 1px solid #111214;
border-radius: 2px;
background: #d5d5d5;
box-shadow: inset 0 0 0 1px rgba(255,255,255,.16);
cursor: pointer;
position: relative;
}
.ncore-tools-setting-row input[type="checkbox"]:checked {
border-color: #658f08;
background: #84bd00;
}
.ncore-tools-setting-row input[type="checkbox"]:checked::after {
content: '';
position: absolute;
left: 3px;
top: 0;
width: 4px;
height: 8px;
border: solid #1d1e21;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.ncore-tools-setting-row input[type="checkbox"]:focus-visible {
outline: 1px solid #84bd00;
outline-offset: 2px;
}
.ncore-tools-setting-label { line-height: 16px; }
.ncore-tools-setting-number-row {
gap: 7px;
max-height: 40px;
overflow: hidden;
padding-left: 31px;
opacity: 1;
cursor: default;
transition: max-height .18s ease, padding .18s ease, opacity .14s ease, border-color .18s ease;
}
.ncore-tools-setting-number-row.is-collapsed {
min-height: 0;
max-height: 0;
padding-top: 0;
padding-bottom: 0;
opacity: 0;
border-top-color: transparent;
pointer-events: none;
}
.ncore-tools-setting-number-row .ncore-tools-setting-label {
flex: 1 1 auto;
}
.ncore-tools-setting-number-input {
width: 54px;
height: 23px;
padding: 2px 5px;
box-sizing: border-box;
border: 1px solid #3b3d42;
border-radius: 3px;
outline: none;
background: #292a2e;
color: #cbCDD0;
font: 10px Verdana, Geneva, Arial, Helvetica, sans-serif;
text-align: center;
}
.ncore-tools-setting-number-input:focus { border-color: #84bd00; }
.ncore-tools-setting-number-input:disabled { opacity: .45; cursor: not-allowed; }
.ncore-tools-setting-number-suffix {
min-width: 10px;
color: #777b80;
font-size: 9px;
text-align: left;
}
#ncore-tools-sync-config {
margin: 8px 0 0;
padding: 9px;
border: 1px solid #303135;
border-radius: 3px;
background: #1e1f22;
}
#ncore-tools-sync-config[hidden] { display: none; }
.ncore-tools-sync-field {
display: grid;
grid-template-columns: 112px minmax(0, 1fr);
align-items: center;
gap: 8px;
margin-top: 6px;
}
.ncore-tools-sync-field:first-child { margin-top: 0; }
.ncore-tools-sync-field span {
color: #777b80;
font-size: 9px;
}
.ncore-tools-sync-field input {
min-width: 0;
height: 25px;
padding: 3px 6px;
box-sizing: border-box;
border: 1px solid #3b3d42;
border-radius: 3px;
outline: none;
background: #292a2e;
color: #cbCDD0;
font: 10px Verdana, Geneva, Arial, Helvetica, sans-serif;
}
.ncore-tools-sync-field input:focus { border-color: #84bd00; }
#ncore-tools-sync-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 8px;
}
#ncore-tools-sync-status {
min-width: 0;
color: #777b80;
font-size: 9px;
line-height: 13px;
}
#ncore-tools-seen-list,
#ncore-tools-download-list {
max-height: min(52vh, 430px);
overflow-y: auto;
border: 1px solid #303135;
border-radius: 3px;
background: #1e1f22;
}
.ncore-tools-seen-row,
.ncore-tools-download-row {
display: flex;
align-items: stretch;
border-top: 1px solid #303135;
}
.ncore-tools-seen-row:first-child,
.ncore-tools-download-row:first-child { border-top: 0; }
.ncore-tools-seen-item,
.ncore-tools-download-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 0;
min-height: 35px;
flex: 1 1 auto;
padding: 6px 9px;
box-sizing: border-box;
color: #adafb2;
text-decoration: none !important;
}
.ncore-tools-seen-item:hover,
.ncore-tools-seen-item:focus-visible,
.ncore-tools-download-item:hover,
.ncore-tools-download-item:focus-visible {
background: #2a2b2f;
color: #cbCDD0;
outline: none;
}
.ncore-tools-seen-delete {
flex: 0 0 auto;
min-width: 58px;
padding: 0 9px;
border: 0;
border-left: 1px solid #303135;
background: transparent;
color: #b66;
font: 9px Verdana, Geneva, Arial, Helvetica, sans-serif;
cursor: pointer;
}
.ncore-tools-seen-delete:hover,
.ncore-tools-seen-delete:focus-visible {
background: #3a2426;
color: #f0a0a0;
outline: none;
}
.ncore-tools-seen-title,
.ncore-tools-download-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 10px;
}
.ncore-tools-seen-imdb {
flex: 0 0 auto;
color: #777b80;
font-size: 9px;
}
.ncore-tools-download-meta {
display: flex;
flex: 0 0 auto;
flex-direction: column;
align-items: flex-end;
gap: 2px;
color: #777b80;
font-size: 9px;
white-space: nowrap;
}
#ncore-tools-seen-empty,
#ncore-tools-download-empty {
padding: 18px 10px;
color: #777b80;
text-align: center;
line-height: 14px;
}
#ncore-tools-settings-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 10px 12px;
background: #2e2f33;
border-top: 1px solid #333437;
}
#ncore-tools-settings-note {
max-width: 265px;
color: #777b80;
font-size: 9px;
line-height: 13px;
}
#ncore-tools-settings-buttons {
display: flex;
flex: 0 0 auto;
gap: 6px;
}
.ncore-tools-settings-button {
min-height: 27px;
padding: 4px 9px;
border: 1px solid #222326;
border-radius: 3px;
background: #34363a;
color: #adafb2;
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
font-size: 10px;
font-weight: bold;
cursor: pointer;
transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.ncore-tools-settings-button:hover {
border-color: #43454c;
background: #3d485f;
color: #fff;
}
.ncore-tools-settings-button.primary {
border-color: #658f08;
background: #84bd00;
color: #1d1e21;
}
.ncore-tools-settings-button.primary:hover {
border-color: #8ec900;
background: #95cf0a;
color: #111214;
}
`;
document.head.appendChild(style);
}
async function openSettingsModal() {
closeSettingsModal();
const syncConfig = await loadSeenSyncConfig();
ensureSettingsModalStyle();
const overlay = document.createElement('div');
overlay.id = 'ncore-tools-settings-overlay';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-labelledby', 'ncore-tools-settings-title');
const panel = document.createElement('div');
panel.id = 'ncore-tools-settings-panel';
const header = document.createElement('div');
header.id = 'ncore-tools-settings-header';
const title = document.createElement('h2');
title.id = 'ncore-tools-settings-title';
title.textContent = 'nCore – Tools beállítások';
const subtitle = document.createElement('div');
subtitle.id = 'ncore-tools-settings-subtitle';
subtitle.textContent = 'Funkciók és szinkronizáció beállítása';
const close = document.createElement('button');
close.id = 'ncore-tools-settings-close';
close.type = 'button';
close.title = 'Bezárás';
close.setAttribute('aria-label', 'Bezárás');
close.textContent = '×';
close.addEventListener('click', closeSettingsModal);
const tabs = document.createElement('div');
tabs.id = 'ncore-tools-settings-tabs';
tabs.setAttribute('role', 'tablist');
tabs.innerHTML = `
`;
header.append(title, subtitle, close, tabs);
panel.appendChild(header);
const form = document.createElement('div');
form.id = 'ncore-tools-settings-form';
const generalPanel = document.createElement('div');
generalPanel.id = 'ncore-tools-panel-general';
generalPanel.className = 'ncore-tools-settings-tab-panel';
generalPanel.setAttribute('role', 'tabpanel');
generalPanel.setAttribute('aria-labelledby', 'ncore-tools-tab-general');
for (const groupConfig of SETTING_GROUPS) {
const group = document.createElement('div');
group.className = 'ncore-tools-setting-group';
group.setAttribute('role', 'group');
group.setAttribute('aria-label', groupConfig.title);
const groupTitle = document.createElement('div');
groupTitle.className = 'ncore-tools-setting-group-title';
groupTitle.textContent = groupConfig.title;
group.appendChild(groupTitle);
for (const [key, labelText] of groupConfig.settings) {
const label = document.createElement('label');
label.className = 'ncore-tools-setting-row';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.dataset.settingKey = key;
checkbox.checked = Boolean(settings[key]);
const text = document.createElement('span');
text.className = 'ncore-tools-setting-label';
text.textContent = labelText;
label.append(checkbox, text);
group.appendChild(label);
if (key === 'highlight') {
const thresholdRow = document.createElement('label');
thresholdRow.className = 'ncore-tools-setting-row ncore-tools-setting-number-row';
const thresholdLabel = document.createElement('span');
thresholdLabel.className = 'ncore-tools-setting-label';
thresholdLabel.textContent = 'Kiemelési küszöb (+)';
const thresholdInput = document.createElement('input');
thresholdInput.type = 'number';
thresholdInput.className = 'ncore-tools-setting-number-input';
thresholdInput.dataset.settingNumberKey = 'highlightMinPlus';
thresholdInput.min = '1';
thresholdInput.step = '1';
thresholdInput.value = String(getHighlightMinPlus());
thresholdInput.title = 'Legalább ennyi + jelölés szükséges a kiemeléshez';
thresholdInput.setAttribute('aria-label', 'Kiemelési küszöb, pluszjelek száma');
function setThresholdExpanded(expanded) {
thresholdRow.classList.toggle('is-collapsed', !expanded);
thresholdRow.setAttribute('aria-hidden', expanded ? 'false' : 'true');
thresholdInput.disabled = !expanded;
}
checkbox.addEventListener('change', () => {
setThresholdExpanded(checkbox.checked);
});
thresholdRow.append(thresholdLabel, thresholdInput);
setThresholdExpanded(checkbox.checked);
group.appendChild(thresholdRow);
}
}
generalPanel.appendChild(group);
}
const seenPanel = document.createElement('div');
seenPanel.id = 'ncore-tools-panel-seen';
seenPanel.className = 'ncore-tools-settings-tab-panel';
seenPanel.setAttribute('role', 'tabpanel');
seenPanel.setAttribute('aria-labelledby', 'ncore-tools-tab-seen');
seenPanel.hidden = true;
function renderSeenMovies() {
const movies = seenSync.getSeenMovies();
seenPanel.replaceChildren();
if (!movies.length) {
const empty = document.createElement('div');
empty.id = 'ncore-tools-seen-empty';
empty.textContent = 'Még nincs látottnak jelölt film.';
seenPanel.appendChild(empty);
return;
}
const list = document.createElement('div');
list.id = 'ncore-tools-seen-list';
for (const movie of movies) {
const row = document.createElement('div');
row.className = 'ncore-tools-seen-row';
const link = document.createElement('a');
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'ncore-tools-seen-item';
link.href = `/torrents.php?mire=${encodeURIComponent(movie.imdbId)}&miben=imdb&tipus=all_own&submit.x=0&submit.y=0&tags=`;
link.title = `${movie.title || `IMDb tt${movie.imdbId}`} keresése az nCore-on`;
const title = document.createElement('span');
title.className = 'ncore-tools-seen-title';
title.textContent = movie.title || `IMDb tt${movie.imdbId}`;
const imdb = document.createElement('span');
imdb.className = 'ncore-tools-seen-imdb';
imdb.textContent = `tt${movie.imdbId}`;
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'ncore-tools-seen-delete';
remove.textContent = 'Törlés';
remove.title = 'Látott jelölés törlése';
remove.addEventListener('click', () => {
const displayTitle = movie.title || `IMDb tt${movie.imdbId}`;
if (!window.confirm(`Biztosan törlöd a látott filmek közül: ${displayTitle}?`)) return;
seenSync.setSeen(movie.imdbId, false, movie.title || '');
renderSeenMovies();
showToast('Film törölve a látottak közül.');
});
link.append(title, imdb);
row.append(link, remove);
list.appendChild(row);
}
seenPanel.appendChild(list);
}
renderSeenMovies();
const downloadsPanel = document.createElement('div');
downloadsPanel.id = 'ncore-tools-panel-downloads';
downloadsPanel.className = 'ncore-tools-settings-tab-panel';
downloadsPanel.setAttribute('role', 'tabpanel');
downloadsPanel.setAttribute('aria-labelledby', 'ncore-tools-tab-downloads');
downloadsPanel.hidden = true;
function renderDownloads() {
const downloads = seenSync.getDownloads();
downloadsPanel.replaceChildren();
if (!downloads.length) {
const empty = document.createElement('div');
empty.id = 'ncore-tools-download-empty';
empty.textContent = 'Még nincs qBittorrenttel hozzáadott torrent.';
downloadsPanel.appendChild(empty);
return;
}
const list = document.createElement('div');
list.id = 'ncore-tools-download-list';
for (const download of downloads) {
const row = document.createElement('div');
row.className = 'ncore-tools-download-row';
const link = document.createElement('a');
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'ncore-tools-download-item';
link.href = `/torrents.php?action=details&id=${encodeURIComponent(download.torrentId)}`;
link.title = `${download.title || `Torrent #${download.torrentId}`} megnyitása`;
const title = document.createElement('span');
title.className = 'ncore-tools-download-title';
title.textContent = download.title || `Torrent #${download.torrentId}`;
const meta = document.createElement('span');
meta.className = 'ncore-tools-download-meta';
const id = document.createElement('span');
id.textContent = `#${download.torrentId}`;
const date = document.createElement('span');
date.textContent = download.ts ? new Date(download.ts).toLocaleString('hu-HU') : '';
meta.append(id, date);
link.append(title, meta);
row.appendChild(link);
list.appendChild(row);
}
downloadsPanel.appendChild(list);
}
renderDownloads();
const syncPanel = document.createElement('div');
syncPanel.id = 'ncore-tools-panel-sync';
syncPanel.className = 'ncore-tools-settings-tab-panel';
syncPanel.setAttribute('role', 'tabpanel');
syncPanel.setAttribute('aria-labelledby', 'ncore-tools-tab-sync');
syncPanel.hidden = true;
const syncBox = document.createElement('div');
syncBox.id = 'ncore-tools-sync-config';
syncBox.innerHTML = `
A szinkronizáció a kitöltött Gist adatokkal automatikusan aktív. A Gist a látott filmeket és a qBittorrenttel hozzáadott torrenteket is tárolja.
`;
syncBox.querySelector('#ncore-tools-gist-token').value = syncConfig.gistToken || '';
syncBox.querySelector('#ncore-tools-gist-id').value = syncConfig.gistId || '';
syncBox.querySelector('#ncore-tools-gist-filename').value = syncConfig.gistFilename || DEFAULT_GIST_FILENAME;
syncPanel.appendChild(syncBox);
syncBox.querySelector('#ncore-tools-sync-now')?.addEventListener('click', async () => {
const status = syncBox.querySelector('#ncore-tools-sync-status');
try {
await saveSeenSyncConfig({
gistToken: syncBox.querySelector('#ncore-tools-gist-token').value,
gistId: syncBox.querySelector('#ncore-tools-gist-id').value,
gistFilename: syncBox.querySelector('#ncore-tools-gist-filename').value,
});
if (status) status.textContent = 'Szinkronizálás…';
const result = await seenSync.syncNow();
if (result.disabled) throw new Error('Hiányzó Gist beállítás.');
if (status) status.textContent = `Kész: ${result.count} látott film, ${result.downloadCount} letöltés.`;
} catch (error) {
if (status) status.textContent = 'Hiba: ' + (error?.message || error);
}
});
form.append(generalPanel, seenPanel, downloadsPanel, syncPanel);
panel.appendChild(form);
function selectTab(tabName) {
const panels = {
general: generalPanel,
seen: seenPanel,
downloads: downloadsPanel,
sync: syncPanel,
};
for (const [name, panelEl] of Object.entries(panels)) {
panelEl.hidden = name !== tabName;
}
tabs.querySelectorAll('.ncore-tools-settings-tab').forEach(tab => {
tab.setAttribute('aria-selected', tab.dataset.tab === tabName ? 'true' : 'false');
});
if (tabName === 'seen') renderSeenMovies();
if (tabName === 'downloads') renderDownloads();
}
tabs.querySelectorAll('.ncore-tools-settings-tab').forEach(tab => {
tab.addEventListener('click', () => selectTab(tab.dataset.tab));
});
const footer = document.createElement('div');
footer.id = 'ncore-tools-settings-footer';
const note = document.createElement('div');
note.id = 'ncore-tools-settings-note';
note.textContent = 'A módosítások mentés után, az oldal újratöltésével lépnek életbe.';
const buttons = document.createElement('div');
buttons.id = 'ncore-tools-settings-buttons';
const cancel = document.createElement('button');
cancel.type = 'button';
cancel.className = 'ncore-tools-settings-button';
cancel.textContent = 'Mégse';
cancel.addEventListener('click', closeSettingsModal);
const save = document.createElement('button');
save.type = 'button';
save.className = 'ncore-tools-settings-button primary';
save.textContent = 'Mentés és újratöltés';
save.addEventListener('click', async () => {
const next = { ...settings };
form.querySelectorAll('input[data-setting-key]').forEach(input => {
next[input.dataset.settingKey] = input.checked;
});
form.querySelectorAll('input[data-setting-number-key]').forEach(input => {
if (input.dataset.settingNumberKey === 'highlightMinPlus') {
next.highlightMinPlus = normalizeHighlightMinPlus(input.value);
}
});
await saveSeenSyncConfig({
gistToken: syncBox.querySelector('#ncore-tools-gist-token').value,
gistId: syncBox.querySelector('#ncore-tools-gist-id').value,
gistFilename: syncBox.querySelector('#ncore-tools-gist-filename').value,
});
await saveSettings(next);
location.reload();
});
buttons.append(cancel, save);
footer.append(note, buttons);
panel.appendChild(footer);
overlay.appendChild(panel);
document.body.appendChild(overlay);
overlay.addEventListener('click', e => {
if (e.target === overlay) closeSettingsModal();
});
settingsKeydownHandler = e => {
if (e.key === 'Escape') closeSettingsModal();
};
document.addEventListener('keydown', settingsKeydownHandler);
close.focus();
}
// -------------------------------------------------------------------------
// Infosáv: [qB URL] [Beállítások]
// -------------------------------------------------------------------------
async function refreshInfosavQBLinkState() {
const a = document.querySelector('#qb-url-link');
if (!a) return;
const qb = await getQBUrl();
if (qb) {
a.style.color = '#2ecc71';
a.title = 'Beállítva: ' + qb + '\nKatt: módosítás';
} else {
a.style.color = '#e74c3c';
a.title = 'Nincs beállítva qB URL\nKatt: beállítás';
}
}
function createBracketLink(id, text, title, onClick) {
const wrapper = document.createElement('span');
wrapper.appendChild(document.createTextNode(' ['));
const a = document.createElement('a');
a.id = id;
a.href = 'javascript:void(0)';
a.textContent = text;
a.title = title;
a.style.textDecoration = 'none';
a.style.fontWeight = 'normal';
a.addEventListener('click', onClick);
wrapper.appendChild(a);
wrapper.appendChild(document.createTextNode(']'));
return wrapper;
}
async function ensureInfosavLinks() {
const infosavAdatok = document.querySelector('#infosav_adatok');
if (!infosavAdatok) return;
function findPremiumInsertAfter() {
const premium = infosavAdatok.querySelector('a.premium[href*="/shop"]');
if (!premium) return null;
let node = premium.nextSibling;
while (node && !(node.nodeType === Node.TEXT_NODE && node.nodeValue.includes(']'))) {
node = node.nextSibling;
}
return node || premium;
}
function insertAfter(node, afterNode) {
const space = document.createTextNode(' ');
if (afterNode && afterNode.parentNode === infosavAdatok) {
infosavAdatok.insertBefore(space, afterNode.nextSibling);
infosavAdatok.insertBefore(node, space.nextSibling);
} else {
infosavAdatok.appendChild(space);
infosavAdatok.appendChild(node);
}
}
let qbLink = document.querySelector('#qb-url-link');
let settingsLink = document.querySelector('#ncore-tools-settings-link');
if (settings.qbittorrent && !qbLink) {
const qbWrapper = createBracketLink(
'qb-url-link',
'qB URL',
'qBittorrent WebUI URL beállítása',
promptForQBUrl
);
if (settingsLink?.parentNode?.parentNode === infosavAdatok) {
const settingsWrapper = settingsLink.parentNode;
infosavAdatok.insertBefore(qbWrapper, settingsWrapper);
infosavAdatok.insertBefore(document.createTextNode(' '), settingsWrapper);
} else {
insertAfter(qbWrapper, findPremiumInsertAfter());
}
qbLink = document.querySelector('#qb-url-link');
}
if (!settingsLink) {
const settingsWrapper = createBracketLink(
'ncore-tools-settings-link',
'Beállítások',
'nCore – Tools funkciók be/ki kapcsolása',
openSettingsModal
);
const qbWrapper = qbLink?.parentNode;
insertAfter(
settingsWrapper,
qbWrapper?.parentNode === infosavAdatok ? qbWrapper : findPremiumInsertAfter()
);
settingsLink = document.querySelector('#ncore-tools-settings-link');
}
if (settings.qbittorrent) await refreshInfosavQBLinkState();
}
// -------------------------------------------------------------------------
// 1) De-dereferer
// -------------------------------------------------------------------------
function initDedereferer() {
const DEREFERERS = [
{ prefix: 'https://dereferer.me/?' },
{ prefix: 'https://dereferer.link/?' },
{ host: 'none.st', param: 'q' },
];
function cleanHref(href) {
for (const dereferer of DEREFERERS) {
if (dereferer.prefix && href.startsWith(dereferer.prefix)) {
return href.slice(dereferer.prefix.length);
}
if (dereferer.host) {
try {
const url = new URL(href);
if (url.hostname === dereferer.host) {
// Nyers cél-URL esetén az & utáni rész is a célhoz tartozik.
const rawTarget = url.search.slice(1).match(
new RegExp(`(?:^|&)${dereferer.param}=(.*)`)
)?.[1];
// Csak a teljesen kódolt célt dekódoljuk; a nyers URL
// saját %-kódolása és + karakterei maradjanak érintetlenek.
let target = rawTarget && /^https?:\/\//i.test(rawTarget)
? rawTarget
: url.searchParams.get(dereferer.param);
if (target && /^https?:\/\//i.test(target)) {
// A külső fragmentet is őrizzük meg, ha a célnak nincs sajátja.
if (url.hash && !target.includes('#')) target += url.hash;
return target;
}
}
} catch (_) {}
}
}
return href;
}
function ensureRel(link) {
const existing = (link.getAttribute('rel') || '').split(/\s+/).filter(Boolean);
for (const value of ['nofollow', 'noreferrer']) {
if (!existing.includes(value)) existing.push(value);
}
link.setAttribute('rel', existing.join(' '));
}
function processLink(link) {
const cleaned = cleanHref(link.href);
if (cleaned !== link.href) {
link.href = cleaned;
ensureRel(link);
}
}
function processNode(node) {
if (node.nodeType !== 1) return;
if (node.matches?.('a[href]')) processLink(node);
node.querySelectorAll?.('a[href]').forEach(processLink);
}
document.querySelectorAll('a[href]').forEach(processLink);
new MutationObserver(mutations => {
for (const mutation of mutations) mutation.addedNodes.forEach(processNode);
}).observe(document.body, { childList: true, subtree: true });
}
// -------------------------------------------------------------------------
// 2) Köszönetek elrejtése
// -------------------------------------------------------------------------
function initNoThanks() {
const THANKS_LABEL = 'Akik eddig megköszönték:';
function updateThanksCount(container) {
if (!(container instanceof HTMLElement)) return;
const count = container.querySelectorAll('a[href*="profile.php?id="]').length;
const description = container.closest('.torrent_leiras');
if (!description) return;
for (const node of description.childNodes) {
if (node === container) break;
if (node.nodeType !== Node.TEXT_NODE || !node.nodeValue?.includes(THANKS_LABEL)) continue;
node.nodeValue = node.nodeValue.replace(
/Akik eddig megköszönték:\s*(?:\d+)?/,
`${THANKS_LABEL} ${count}`
);
break;
}
}
function hideThanks(root = document) {
const containers = [];
if (root instanceof HTMLElement && root.id === 'ncoreKoszonetAjax') containers.push(root);
root.querySelectorAll?.('#ncoreKoszonetAjax').forEach(container => containers.push(container));
for (const container of containers) {
updateThanksCount(container);
container.remove();
}
}
hideThanks();
new MutationObserver(mutations => {
for (const mutation of mutations) {
mutation.addedNodes.forEach(node => {
if (node instanceof HTMLElement) hideThanks(node);
});
}
}).observe(document.body, { childList: true, subtree: true });
}
// -------------------------------------------------------------------------
// 3) Reklámok eltávolítása
// -------------------------------------------------------------------------
function initRemoveAds() {
const AD_SELECTORS = [];
const isMainPage = location.pathname === '/' || location.pathname.endsWith('/index.php');
const isTorrentPage = location.pathname.endsWith('/torrents.php');
if (isMainPage) {
AD_SELECTORS.push('.hessteg-ad-block', '.news_block_right iframe');
}
if (isTorrentPage) AD_SELECTORS.push('.banner', '#hessteg');
if (!AD_SELECTORS.length || document.getElementById('ncore-tools-remove-ads-style')) return;
const style = document.createElement('style');
style.id = 'ncore-tools-remove-ads-style';
style.textContent = `${AD_SELECTORS.join(',\n')} { display: none !important; }`;
document.head.appendChild(style);
}
// -------------------------------------------------------------------------
// 4) Képek beágyazása a torrent lenyíló leírásába
// -------------------------------------------------------------------------
function initEmbedImages() {
if (!location.pathname.endsWith('/torrents.php')) return;
const params = new URLSearchParams(location.search);
if (params.get('action')) return;
function requestText(url) {
const absoluteUrl = new URL(url, window.location.href).href;
if (typeof GM_xmlhttpRequest === 'function') {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: absoluteUrl,
responseType: 'text',
timeout: 15000,
onload: response => {
if (response.status >= 200 && response.status < 400) {
resolve(response.responseText || '');
} else {
reject(new Error(`HTTP ${response.status}: ${absoluteUrl}`));
}
},
onerror: () => reject(new Error(`Hálózati hiba: ${absoluteUrl}`)),
ontimeout: () => reject(new Error(`Időtúllépés: ${absoluteUrl}`)),
});
});
}
return fetch(absoluteUrl, { credentials: 'include' }).then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}: ${absoluteUrl}`);
return response.text();
});
}
function findImageUrl(html, pageUrl) {
try {
const doc = new DOMParser().parseFromString(html, 'text/html');
const images = Array.from(doc.querySelectorAll('img[src]'));
const matching = images.filter(img => /\.(?:jpe?g|png|bmp|gif|tiff?)(?:[?#]|$)/i.test(img.getAttribute('src') || ''));
const image = matching.at(-1) || images.at(-1);
if (!image) return '';
return new URL(image.getAttribute('src'), pageUrl).href;
} catch (_) {
const match = String(html).match(/
]+src=["']([^"']+\.(?:jpe?g|png|bmp|gif|tiff?)(?:[?#][^"']*)?)["']/i);
if (!match) return '';
try { return new URL(match[1], pageUrl).href; } catch (_) { return match[1]; }
}
}
function stylePreviewImage(image, maxWidth = '280px', maxHeight = '500px') {
Object.assign(image.style, {
border: '0px',
width: 'auto',
height: 'auto',
maxWidth,
maxHeight,
});
}
function ensureFancyBoxStyle() {
if (document.getElementById('ncore-tools-fancybox-style')) return;
const style = document.createElement('style');
style.id = 'ncore-tools-fancybox-style';
style.textContent = `
#fancybox-overlay,
.fancybox-overlay {
position: fixed !important;
inset: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 110000 !important;
opacity: 1 !important;
background: rgba(0, 0, 0, .45) !important;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
#fancybox-wrap,
.fancybox-wrap,
.fancybox-container {
z-index: 110010 !important;
}
#fancybox-loading,
.fancybox-loading {
z-index: 110020 !important;
}
`;
document.head.appendChild(style);
}
ensureFancyBoxStyle();
function initializeFancyBox(root) {
try {
const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const jq = pageWindow.jQuery || pageWindow.$;
if (typeof jq !== 'function' || typeof jq.fn?.fancybox !== 'function') return;
const links = Array.from(root.querySelectorAll('a.fancy_groups'))
.filter(link => link.dataset.ncoreFancyBoxBound !== '1');
if (!links.length) return;
jq(links).fancybox({
type: 'image',
...(typeof pageWindow.disableKeys === 'function' ? { onStart: pageWindow.disableKeys } : {}),
...(typeof pageWindow.enableKeys === 'function' ? { onClosed: pageWindow.enableKeys } : {}),
});
links.forEach(link => { link.dataset.ncoreFancyBoxBound = '1'; });
} catch (error) {
console.debug('[nCore Tools] FancyBox init kihagyva:', error);
}
}
async function processAttachedPreview(preview) {
if (!(preview instanceof HTMLImageElement) || preview.dataset.ncoreEmbedProcessed === '1') return;
preview.dataset.ncoreEmbedProcessed = '1';
const anchor = preview.closest('a[href]');
if (!anchor) return;
stylePreviewImage(preview);
anchor.parentElement?.removeAttribute('class');
if (anchor.classList.contains('fancy_groups')) {
preview.src = anchor.href;
return;
}
anchor.target = '_blank';
anchor.rel = [anchor.rel, 'noopener', 'noreferrer'].filter(Boolean).join(' ');
try {
const html = await requestText(anchor.href);
const imageUrl = findImageUrl(html, anchor.href);
if (imageUrl) preview.src = imageUrl;
} catch (error) {
console.debug('[nCore Tools] Előnézeti kép lekérése sikertelen:', error);
}
}
function revealSpoilerForImage(anchor) {
const spoiler = anchor.closest('.bb-quote');
if (!spoiler) return;
const previous = spoiler.previousSibling;
if (previous?.parentNode === spoiler.parentNode && previous.nodeType === Node.ELEMENT_NODE) {
previous.remove();
}
const first = spoiler.firstElementChild;
if (first && /spoiler|quote/i.test(first.className || '')) first.remove();
Array.from(spoiler.children).forEach(child => {
if (getComputedStyle(child).display === 'none') child.style.display = 'block';
});
spoiler.classList.remove('bb-quote');
}
function processDescriptionImage(description, fancyMode, groupRel) {
if (!(description instanceof Element) || description.dataset.ncoreEmbedProcessed === '1') return;
const anchor = description.closest('a[href]');
if (!anchor || anchor.title !== 'Csatolt kép megnyitása') return;
description.dataset.ncoreEmbedProcessed = '1';
const imageUrl = anchor.href;
anchor.removeAttribute('title');
revealSpoilerForImage(anchor);
if (!anchor.querySelector('.ncore-embedded-description-image')) {
const image = document.createElement('img');
image.className = 'ncore-embedded-description-image';
image.src = imageUrl;
image.alt = description.getAttribute('alt') || 'Csatolt kép';
stylePreviewImage(image, '600px', '400px');
anchor.appendChild(image);
}
description.style.display = 'none';
anchor.target = fancyMode ? '' : '_blank';
if (!fancyMode) anchor.rel = [anchor.rel, 'noopener', 'noreferrer'].filter(Boolean).join(' ');
if (fancyMode) {
anchor.classList.add('fancy_groups');
if (groupRel) anchor.setAttribute('rel', groupRel);
const parent = anchor.parentElement;
if (parent && !parent.querySelector('.ncore-embed-open-new')) {
const openLink = document.createElement('a');
openLink.className = 'ncore-embed-open-new';
openLink.href = imageUrl;
openLink.target = '_blank';
openLink.rel = 'noopener noreferrer';
openLink.textContent = 'Megnyitás új ablakban...';
openLink.style.fontStyle = 'italic';
openLink.style.display = 'block';
parent.appendChild(openLink);
}
}
}
function processEmbedRoot(root) {
if (!(root instanceof Element)) return;
const previews = Array.from(root.querySelectorAll('img.attached_link'));
const fancyPreviews = previews.filter(preview => preview.closest('a[href]')?.classList.contains('fancy_groups'));
const fancyMode = previews.length > 0 && fancyPreviews.length === previews.length;
const groupRel = fancyPreviews[0]?.closest('a[href]')?.getAttribute('rel') || '';
previews.forEach(preview => processAttachedPreview(preview));
root.querySelectorAll('.kepmeret_txt').forEach(size => {
size.style.textAlign = 'center';
});
Array.from(root.querySelectorAll('.description')).forEach(description => {
processDescriptionImage(description, fancyMode, groupRel);
});
if (fancyMode) initializeFancyBox(root);
}
function findDropRoot(node) {
let current = node instanceof Element ? node : node?.parentElement;
while (current && current !== document.body) {
if (/^\d+$/.test(current.id || '') && current.querySelector('.torrent_lenyilo_tartalom')) {
return current;
}
current = current.parentElement;
}
return node instanceof Element ? node : null;
}
document.querySelectorAll('.torrent_lenyilo_tartalom').forEach(content => {
processEmbedRoot(findDropRoot(content) || content);
});
new MutationObserver(mutations => {
const roots = new Set();
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof Element)) continue;
if (node.matches('img.attached_link, .description, .torrent_lenyilo_tartalom')) {
roots.add(findDropRoot(node) || node.parentElement || node);
}
if (node.querySelector?.('img.attached_link, .description, .torrent_lenyilo_tartalom')) {
roots.add(findDropRoot(node) || node);
}
}
}
roots.forEach(processEmbedRoot);
}).observe(document.body, { childList: true, subtree: true });
}
// -------------------------------------------------------------------------
// 5) Láttam már
// -------------------------------------------------------------------------
function initSeen() {
if (!location.pathname.endsWith('/torrents.php')) return;
const params = new URLSearchParams(location.search);
const action = params.get('action');
const TORRENT_SELECTOR = '.box_torrent';
const SEEN_CLASS = 'ncore-seen';
function normalizeTitle(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function initDetailsSeenButton() {
if (action !== 'details') return;
// Csak filmek jelölhetők. Sorozatnál vagy más torrenttípusnál nincs gomb.
const isMovie = Boolean(
document.querySelector('.torrent_reszletek .torrent_col1 a[href*="csoport_listazas=osszes_film"]')
);
if (!isMovie) return;
const imdbLink = document.querySelector('.torrent_leiras a[href*="imdb.com/title/tt"]');
const imdbMatch = imdbLink?.href?.match(/tt(\d+)/);
const imdbId = imdbMatch ? imdbMatch[1] : null;
if (!imdbId) return;
const container = document.querySelector('.torrent_reszletek_konyvjelzo');
if (!container || container.querySelector('.ncore-seen-details-btn')) return;
function getDetailsMovieTitle() {
const titleEl = document.querySelector('.infobar_title');
if (titleEl) {
let firstLine = '';
for (const node of titleEl.childNodes) {
if (node.nodeName === 'BR') break;
firstLine += node.textContent || '';
}
const movieTitle = normalizeTitle(firstLine);
if (movieTitle) return movieTitle;
}
return normalizeTitle(document.querySelector('.torrent_reszletek_cim')?.textContent);
}
const seenLink = document.createElement('a');
seenLink.href = 'javascript:void(0);';
seenLink.className = 'ncore-seen-details-btn';
seenLink.style.fontWeight = 'normal';
function updateDetailsButton() {
const seen = seenSync.isSeen(imdbId);
seenLink.textContent = seen ? '[✓ láttam már]' : '[láttam már]';
seenLink.title = seen
? 'Látott jelölés visszavonása'
: 'Megjelölés látott filmként';
seenLink.style.color = seen ? '#84bd00' : '';
}
seenLink.addEventListener('click', event => {
event.preventDefault();
const nextSeen = !seenSync.isSeen(imdbId);
seenSync.setSeen(imdbId, nextSeen, getDetailsMovieTitle());
updateDetailsButton();
showToast(nextSeen ? 'Film megjelölve látottként.' : 'Látott jelölés visszavonva.');
});
updateDetailsButton();
container.appendChild(document.createTextNode(' '));
container.appendChild(seenLink);
}
if (action) {
initDetailsSeenButton();
return;
}
const style = document.createElement('style');
style.textContent = `
.box_torrent.${SEEN_CLASS} { opacity: 0.5; }
body > [id^="borito"] { opacity: 1 !important; }
`;
document.head.appendChild(style);
function getImdbId(row) {
const link = row.querySelector('.infolink');
const match = link?.href?.match(/tt(\d+)/);
return match ? match[1] : null;
}
function getMovieTitle(row) {
const titleSpan = row.querySelector('.torrent_txt .siterank span[title]');
const movieTitle = normalizeTitle(titleSpan?.getAttribute('title') || titleSpan?.textContent);
if (movieTitle) return movieTitle;
const torrentLink = row.querySelector('.torrent_txt > a[href*="action=details"]');
return normalizeTitle(torrentLink?.getAttribute('title') || torrentLink?.textContent);
}
function isSeries(row) {
const categ = row.querySelector('.categ_link');
return Boolean(categ && /sorozat/i.test(categ.title));
}
function updateRow(row) {
const imdbId = getImdbId(row);
if (!imdbId) return;
const seen = seenSync.isSeen(imdbId);
row.classList.toggle(SEEN_CLASS, seen);
// Ha a poszterkártya már felépült, a rajta lévő jelölőgombot is
// azonnal szinkronban tartjuk a lista nézet dupla kattintásával.
const posterButton = row.querySelector(':scope > .ncore-poster-card .ncore-poster-action[data-seen-button="1"]');
if (posterButton) {
posterButton.classList.toggle('active', seen);
posterButton.title = seen ? 'Látott jelölés visszavonása' : 'Megjelölés látott filmként';
posterButton.setAttribute('aria-label', posterButton.title);
}
}
function toggleSeen(row) {
const imdbId = getImdbId(row);
if (!imdbId) return;
seenSync.setSeen(imdbId, !seenSync.isSeen(imdbId), getMovieTitle(row));
document.querySelectorAll(TORRENT_SELECTOR).forEach(updateRow);
}
function bindRow(row) {
if (!(row instanceof HTMLElement) || row.dataset.ncoreSeenBound === '1') return;
row.dataset.ncoreSeenBound = '1';
if (isSeries(row)) return;
updateRow(row);
row.addEventListener('dblclick', event => {
event.preventDefault();
toggleSeen(row);
});
row.querySelectorAll('[id^="borito"]').forEach(cover => document.body.appendChild(cover));
}
document.querySelectorAll(TORRENT_SELECTOR).forEach(bindRow);
new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
if (node.matches(TORRENT_SELECTOR)) bindRow(node);
node.querySelectorAll?.(TORRENT_SELECTOR).forEach(bindRow);
}
}
}).observe(document.body, { childList: true, subtree: true });
}
// -------------------------------------------------------------------------
// 6) Torrentlista nezetvalto: lista / poszter
// -------------------------------------------------------------------------
async function initTorrentViewSwitcher() {
if (!location.pathname.endsWith('/torrents.php')) return;
const params = new URLSearchParams(location.search);
if (params.get('action')) return;
const torrentContainer = document.querySelector('.box_torrent_all');
const listaAll = torrentContainer?.closest('.lista_all') || document.querySelector('.lista_all');
const listaHeader = listaAll?.querySelector(':scope > .lista_fej') || listaAll?.querySelector('.lista_fej');
if (!torrentContainer || !listaAll || !listaHeader) return;
const VIEW_LIST = 'list';
const VIEW_POSTER = 'poster';
const VALID_VIEWS = new Set([VIEW_LIST, VIEW_POSTER]);
let currentView = String(await gmGet(GM_KEY_TORRENT_VIEW, VIEW_LIST) || VIEW_LIST);
if (!VALID_VIEWS.has(currentView)) currentView = VIEW_LIST;
const style = document.createElement('style');
style.id = 'ncore-tools-torrent-view-style';
style.textContent = `
.ncore-torrent-view-switcher-host {
position: relative;
height: 0;
margin: 0 auto;
overflow: visible;
pointer-events: none;
}
#ncore-torrent-view-switcher {
position: absolute;
right: 10px;
z-index: 3;
display: flex;
flex-direction: row;
gap: 2px;
padding: 2px;
border: 1px solid #35363a;
border-radius: 3px;
background: rgba(34, 35, 38, .94);
box-shadow: 0 1px 5px rgba(0, 0, 0, .35);
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
line-height: normal;
transform: translateY(-50%);
pointer-events: auto;
}
.ncore-torrent-view-button {
display: flex;
width: 68px;
height: 22px;
align-items: center;
justify-content: center;
gap: 5px;
padding: 0 6px;
border: 1px solid transparent;
border-radius: 2px;
background: transparent;
color: #8b8e92;
font: bold 9px/1 Verdana, Geneva, Arial, Helvetica, sans-serif;
text-align: center;
cursor: pointer;
box-sizing: border-box;
transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.ncore-torrent-view-button:hover,
.ncore-torrent-view-button:focus-visible {
border-color: #43454c;
background: #303236;
color: #d4d6d8;
outline: none;
}
.ncore-torrent-view-button[aria-pressed="true"] {
border-color: #658f08;
background: #84bd00;
color: #1d1e21;
}
.ncore-torrent-view-button .ncore-lucide-icon {
display: block;
width: 13px;
height: 13px;
flex: 0 0 13px;
pointer-events: none;
stroke: currentColor;
}
.ncore-torrent-view-label { pointer-events: none; }
.ncore-poster-card { display: none; }
body.ncore-poster-view .box_alcimek_all { display: none !important; }
body.ncore-poster-view .box_torrent_all {
display: grid !important;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
align-items: stretch;
width: 902px !important;
max-width: 100%;
height: auto !important;
margin: 0 auto;
padding: 12px;
overflow: visible !important;
box-sizing: border-box;
}
body.ncore-poster-view .box_torrent_all > :not(.box_torrent) {
display: none !important;
}
body.ncore-poster-modal-open {
overflow: hidden !important;
}
#ncore-poster-details-overlay {
position: fixed;
inset: 0;
z-index: 90000;
display: none;
align-items: center;
justify-content: center;
padding: 28px;
background: rgba(8, 9, 10, .82);
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
box-sizing: border-box;
}
#ncore-poster-details-overlay.is-open {
display: flex;
}
#ncore-poster-details-modal {
display: flex;
width: min(1080px, calc(100vw - 56px));
max-width: 100%;
max-height: calc(100vh - 56px);
min-height: 0;
flex-direction: column;
overflow: hidden;
border: 1px solid #414348;
border-top: 2px solid #84bd00;
border-radius: 6px;
background: #222326;
box-shadow: 0 18px 60px rgba(0, 0, 0, .72);
color: #adafb2;
box-sizing: border-box;
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
}
#ncore-poster-details-header {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 12px;
min-height: 44px;
padding: 8px 10px 8px 14px;
border-bottom: 1px solid #383a3e;
background: #2a2b2f;
box-sizing: border-box;
}
#ncore-poster-details-title {
min-width: 0;
flex: 1 1 auto;
overflow: hidden;
color: #d4d6d8;
font-size: 11px;
font-weight: bold;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
#ncore-poster-details-open {
flex: 0 0 auto;
color: #84bd00 !important;
font-size: 9px;
text-decoration: none !important;
white-space: nowrap;
}
#ncore-poster-details-open:hover,
#ncore-poster-details-open:focus-visible {
color: #a6da2e !important;
text-decoration: underline !important;
outline: none;
}
#ncore-poster-details-close {
display: flex;
width: 28px;
height: 28px;
flex: 0 0 28px;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: #8b8e92;
font: normal 20px/1 Arial, sans-serif;
cursor: pointer;
}
#ncore-poster-details-close:hover,
#ncore-poster-details-close:focus-visible {
border-color: #4a4c51;
background: #34363a;
color: #fff;
outline: none;
}
#ncore-poster-details-content {
min-height: 0;
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
padding: 12px;
box-sizing: border-box;
}
#ncore-poster-details-content > .ncore-poster-drop {
position: static !important;
display: block !important;
float: none !important;
clear: both !important;
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
height: auto !important;
margin: 0 !important;
overflow: visible !important;
box-sizing: border-box !important;
}
#ncore-poster-details-content .ncore-poster-drop > .torrent_lenyilo_lehetoseg,
#ncore-poster-details-content .ncore-poster-drop > .torrent_lenyilo_tartalom,
#ncore-poster-details-content .ncore-poster-drop > .torrent_lenyilo_lab {
position: static !important;
left: auto !important;
right: auto !important;
float: none !important;
width: 100% !important;
max-width: 100% !important;
margin-left: 0 !important;
margin-right: 0 !important;
box-sizing: border-box !important;
}
#ncore-poster-details-content .ncore-poster-drop > .torrent_lenyilo_lehetoseg {
overflow: hidden !important;
}
#ncore-poster-details-content .ncore-poster-drop .torrent_lenyilo_tartalom img,
#ncore-poster-details-content .ncore-poster-drop .torrent_lenyilo_tartalom iframe,
#ncore-poster-details-content .ncore-poster-drop .torrent_lenyilo_tartalom table {
max-width: 100%;
}
body.ncore-poster-view .box_torrent.ncore-poster-expanded .ncore-poster-card {
border-color: #84bd00;
}
body.ncore-poster-view .box_torrent {
position: relative !important;
display: block !important;
float: none !important;
width: auto !important;
min-width: 0 !important;
height: auto !important;
min-height: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: visible !important;
opacity: 1;
border: 0 !important;
background: transparent !important;
box-sizing: border-box;
}
body.ncore-poster-view .box_torrent > :not(.ncore-poster-card) {
display: none !important;
}
body.ncore-poster-view .box_torrent.ncore-seen .ncore-poster-card {
opacity: .48;
}
body.ncore-poster-view .ncore-poster-card {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
min-height: 330px;
overflow: hidden;
border: 1px solid #34363a;
border-radius: 5px;
background: #222326;
box-shadow: 0 4px 14px rgba(0, 0, 0, .28);
color: #adafb2;
box-sizing: border-box;
transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
}
body.ncore-poster-view .ncore-poster-card:hover {
z-index: 2;
border-color: #51545a;
box-shadow: 0 8px 22px rgba(0, 0, 0, .42);
}
body.ncore-poster-view .ncore-poster-card.ncore-poster-plus {
background: #600A0A;
}
body.ncore-poster-view .ncore-poster-card.ncore-poster-plus:hover {
border-color: #51545a;
}
body.ncore-poster-view .box_torrent.ncore-poster-expanded .ncore-poster-card:hover {
border-color: #84bd00;
box-shadow: 0 8px 22px rgba(0, 0, 0, .42);
}
.ncore-poster-media {
position: relative;
display: block;
aspect-ratio: 2 / 3;
overflow: hidden;
background: #17181a;
}
.ncore-poster-image-link {
position: absolute;
inset: 0;
display: block;
overflow: hidden;
background: #17181a;
text-decoration: none !important;
}
.ncore-poster-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
border: 0;
transition: transform .2s ease, filter .2s ease;
}
.ncore-poster-card:hover .ncore-poster-image {
transform: scale(1.025);
}
.ncore-poster-placeholder {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
color: #5f6267;
font: 11px/16px Verdana, Geneva, Arial, Helvetica, sans-serif;
text-align: center;
}
.ncore-poster-badges {
position: absolute;
left: 8px;
bottom: 8px;
z-index: 3;
display: flex;
max-width: calc(100% - 16px);
align-items: center;
gap: 6px;
pointer-events: none;
}
.ncore-poster-category-badge {
display: inline-flex;
flex: 0 0 51px;
width: 51px;
height: 28px;
align-items: center;
justify-content: center;
padding: 0;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, .16);
border-radius: 4px;
background: rgba(17, 18, 20, .9);
box-shadow: 0 2px 7px rgba(0, 0, 0, .55);
text-decoration: none !important;
box-sizing: border-box;
pointer-events: auto;
transition: border-color .15s ease, background .15s ease;
}
.ncore-poster-category-badge:hover,
.ncore-poster-category-badge:focus-visible {
border-color: #84bd00;
background: rgba(28, 30, 32, .98);
outline: none;
}
.ncore-poster-category-image {
display: block;
flex: 0 0 auto;
width: auto;
height: auto;
max-width: none;
max-height: none;
object-fit: none;
object-position: center;
border: 0;
}
.ncore-poster-quality-badge {
display: inline-flex;
flex: 0 0 auto;
min-height: 28px;
align-items: center;
justify-content: center;
padding: 0 8px;
border: 1px solid rgba(132, 189, 0, .62);
border-radius: 4px;
background: rgba(17, 18, 20, .9);
box-shadow: 0 2px 7px rgba(0, 0, 0, .55);
color: #b9df5c;
font: bold 10px/1 Verdana, Geneva, Arial, Helvetica, sans-serif;
letter-spacing: .1px;
white-space: nowrap;
box-sizing: border-box;
pointer-events: none;
}
.ncore-poster-imdb-badge {
display: inline-flex;
flex: 0 0 auto;
min-height: 28px;
align-items: center;
justify-content: center;
padding: 0 8px;
border: 1px solid rgba(214, 184, 90, .65);
border-radius: 4px;
background: rgba(17, 18, 20, .9);
box-shadow: 0 2px 7px rgba(0, 0, 0, .55);
color: #e4c966 !important;
font: bold 10px/1 Verdana, Geneva, Arial, Helvetica, sans-serif;
letter-spacing: .1px;
white-space: nowrap;
text-decoration: none !important;
box-sizing: border-box;
pointer-events: auto;
transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.ncore-poster-imdb-badge:hover,
.ncore-poster-imdb-badge:focus-visible {
border-color: #f0d46f;
background: rgba(28, 30, 32, .98);
color: #f0d46f !important;
outline: none;
}
.ncore-poster-body {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: 6px;
min-width: 0;
padding: 9px 10px 10px;
}
.ncore-poster-title {
display: -webkit-box;
overflow: hidden;
color: #d2d4d6 !important;
font: bold 11px/15px Verdana, Geneva, Arial, Helvetica, sans-serif;
text-decoration: none !important;
overflow-wrap: anywhere;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.ncore-poster-title:hover,
.ncore-poster-title:focus-visible {
color: #84bd00 !important;
outline: none;
}
.ncore-poster-movie-title {
min-height: 13px;
overflow: hidden;
color: #aeb1b5;
font: 10px/13px Verdana, Geneva, Arial, Helvetica, sans-serif;
text-overflow: ellipsis;
white-space: nowrap;
}
.ncore-poster-meta {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
margin-top: auto;
color: #8a8d91;
font: 9px/13px Verdana, Geneva, Arial, Helvetica, sans-serif;
}
.ncore-poster-meta span { white-space: nowrap; }
.ncore-poster-meta .seed { color: #84bd00; }
.ncore-poster-meta .leech { color: #d17b70; }
.ncore-poster-meta .imdb {
color: #d6b85a;
text-decoration: none;
}
.ncore-poster-meta a.imdb:hover,
.ncore-poster-meta a.imdb:focus-visible {
color: #f0d46f;
text-decoration: underline;
outline: none;
}
.ncore-poster-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(32px, 1fr));
gap: 5px;
margin-top: 2px;
padding-top: 8px;
border-top: 1px solid #303236;
}
.ncore-poster-action {
display: flex !important;
width: 100%;
min-width: 0;
height: 29px;
margin: 0 !important;
align-items: center;
justify-content: center;
padding: 0 7px;
border: 1px solid #3a3c40;
border-radius: 4px;
background: #292b2f;
color: #c5c7ca !important;
font: bold 10px/1 Verdana, Geneva, Arial, Helvetica, sans-serif;
vertical-align: middle;
text-align: center;
text-decoration: none !important;
cursor: pointer;
box-sizing: border-box;
appearance: none;
-webkit-appearance: none;
transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.ncore-poster-action:hover,
.ncore-poster-action:focus-visible {
border-color: #84bd00;
background: #303236;
color: #84bd00 !important;
outline: none;
}
.ncore-poster-action.active {
border-color: #658f08;
background: #84bd00;
color: #1d1e21 !important;
}
.ncore-poster-action.qb {
letter-spacing: -.2px;
}
.ncore-poster-action .ncore-lucide-icon {
display: block;
width: 16px;
height: 16px;
flex: 0 0 16px;
pointer-events: none;
stroke: currentColor;
}
.ncore-poster-action.seen {
padding-inline: 7px;
}
`;
document.head.appendChild(style);
function normalizeText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function getTorrentQualityLabel(title) {
const text = normalizeText(title);
if (!text) return '';
const qualityPatterns = [
[/\b2160p\b/i, '2160p'],
[/\b(?:4k|uhd)\b/i, '4K'],
[/\b1080p\b/i, '1080p'],
[/\b1080i\b/i, '1080i'],
[/\b720p\b/i, '720p'],
[/\b576p\b/i, '576p'],
[/\b480p\b/i, '480p'],
[/\bxvid\b/i, 'XviD'],
[/\bdvdrip\b/i, 'DVDRip'],
[/\bdvd(?:5|9)\b/i, match => match[0].toUpperCase()],
];
for (const [pattern, label] of qualityPatterns) {
const match = text.match(pattern);
if (!match) continue;
return typeof label === 'function' ? label(match) : label;
}
return '';
}
function getDetailsLink(row) {
return row.querySelector(
'.torrent_txt > a[href*="action=details"], ' +
'.torrent_txt2 > a[href*="action=details"]'
);
}
function getPosterUrl(row) {
const info = row.querySelector('img.infobar_ico');
const handler = info?.getAttribute('onmouseover') || '';
const match = handler.match(/mutat\(\s*['"]([^'"]+)['"]/i);
if (match?.[1]) {
try {
const url = new URL(match[1], window.location.href);
if (/^https?:$/.test(url.protocol)) return url.href;
} catch (_) {}
}
const existing = row.querySelector(
'img.ncore-poster-image, img[src*="cover" i], img[src*="borito" i]'
);
if (existing?.src && /^https?:/i.test(existing.src)) return existing.src;
return '';
}
function getImdbId(row) {
const link = row.querySelector('.infolink[href*="imdb.com/title/tt"]');
const match = link?.href?.match(/tt(\d+)/);
return match ? match[1] : '';
}
function getImdbRating(row) {
const link = row.querySelector('.infolink[href*="imdb.com/title/tt"]');
const text = normalizeText(link?.textContent);
const match = text.match(/imdb\s*:\s*(\d+(?:[.,]\d+)?)/i);
return match ? match[1].replace(',', '.') : '';
}
function getImdbUrl(row) {
const link = row.querySelector('.infolink[href*="imdb.com/title/tt"]');
return link?.href || '';
}
function isSeries(row) {
const category = row.querySelector('.categ_link');
return Boolean(category && /sorozat/i.test(category.title || category.textContent || ''));
}
function getCellText(row, selectors) {
for (const selector of selectors) {
const value = normalizeText(row.querySelector(selector)?.textContent);
if (value) return value;
}
return '';
}
function normalizePosterUploaded(value) {
return normalizeText(value).replace(
/^(\d{4}-\d{2}-\d{2})(?=\d{2}:\d{2}:\d{2}(?:\b|$))/,
'$1 '
);
}
function getMovieTitle(row) {
const titleSpan = row.querySelector(
'.torrent_txt .siterank span[title], ' +
'.torrent_txt2 .siterank span[title]'
);
return normalizeText(titleSpan?.getAttribute('title') || titleSpan?.textContent);
}
// Ugyanaz a címválasztás, mint a lista nézet „Láttam már” funkciójában:
// ha az nCore megad külön filmcímet a siterank sorban, azt mentjük;
// egyébként visszaesünk a torrent nevére.
function getSeenMovieTitle(row) {
const movieTitle = getMovieTitle(row);
if (movieTitle) return movieTitle;
const torrentLink = getDetailsLink(row);
return normalizeText(torrentLink?.getAttribute('title') || torrentLink?.textContent);
}
function getTorrentId(detailsUrl) {
try {
return new URL(detailsUrl, window.location.href).searchParams.get('id') || '';
} catch (_) {
return '';
}
}
function getDownloadKey() {
const alternate = document.querySelector('link[rel="alternate"][href*="key="]');
if (!alternate) return '';
try {
return new URL(alternate.href, window.location.href).searchParams.get('key') || '';
} catch (_) {
return '';
}
}
function getTorrentDownloadUrl(row, torrentId) {
const existingLink = row?.querySelector?.('a[href*="torrents.php?action=download"]');
if (existingLink) {
try {
return new URL(existingLink.getAttribute('href'), window.location.href).href;
} catch (_) {}
}
const key = getDownloadKey();
if (!torrentId || !key) return '';
return `${window.location.origin}/torrents.php?action=download&id=${encodeURIComponent(torrentId)}&key=${encodeURIComponent(key)}`;
}
// Lucide ikonok inline SVG-ként, külső runtime nélkül.
// Forrás: https://lucide.dev/icons/download, /bookmark, /check,
// /layout-grid, /text-align-justify
const LUCIDE_ICONS = {
download: [
['path', { d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }],
['path', { d: 'm7 10 5 5 5-5' }],
['path', { d: 'M12 15V3' }],
],
bookmark: [
['path', { d: 'm19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z' }],
],
check: [
['path', { d: 'M20 6 9 17l-5-5' }],
],
'layout-grid': [
['rect', { width: '7', height: '7', x: '3', y: '3', rx: '1' }],
['rect', { width: '7', height: '7', x: '14', y: '3', rx: '1' }],
['rect', { width: '7', height: '7', x: '14', y: '14', rx: '1' }],
['rect', { width: '7', height: '7', x: '3', y: '14', rx: '1' }],
],
'text-align-justify': [
['path', { d: 'M3 6h18' }],
['path', { d: 'M3 12h18' }],
['path', { d: 'M3 18h18' }],
],
};
function createLucideIcon(name) {
const nodes = LUCIDE_ICONS[name];
if (!nodes?.length) return document.createTextNode('');
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.classList.add('ncore-lucide-icon');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '16');
svg.setAttribute('height', '16');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('focusable', 'false');
for (const [tagName, attributes] of nodes) {
const node = document.createElementNS('http://www.w3.org/2000/svg', tagName);
for (const [attribute, value] of Object.entries(attributes)) {
node.setAttribute(attribute, value);
}
svg.appendChild(node);
}
return svg;
}
function setupPosterBookmarkButton(button) {
if (!button) return;
button.classList.remove('active');
button.replaceChildren(createLucideIcon('bookmark'));
button.title = 'Könyvjelzőhöz adás';
button.setAttribute('aria-label', button.title);
}
function createMetaSpan(className, text, title = '') {
if (!text) return null;
const span = document.createElement('span');
if (className) span.className = className;
span.textContent = text;
if (title) span.title = title;
return span;
}
function updatePosterSeenState(row, button, imdbId) {
if (!button || !imdbId) return;
const seen = seenSync.isSeen(imdbId);
button.classList.toggle('active', seen);
if (!button.querySelector('.ncore-lucide-icon')) {
button.replaceChildren(createLucideIcon('check'));
}
button.title = seen ? 'Látott jelölés visszavonása' : 'Megjelölés látott filmként';
button.setAttribute('aria-label', button.title);
row.classList.toggle('ncore-seen', seen);
}
const posterDropOrigins = new Map();
let posterModalElements = null;
let posterModalState = null;
let posterModalRequestId = 0;
function isPosterDropElement(element) {
return Boolean(element) && (
element.classList.contains('torrent_lenyilo') ||
element.classList.contains('torrent_lenyilo2')
);
}
function getPosterDrop(torrentId) {
if (!torrentId) return null;
const drop = document.getElementById(String(torrentId));
return isPosterDropElement(drop) ? drop : null;
}
function rememberPosterDropOrigin(drop) {
if (!drop?.parentNode || posterDropOrigins.has(drop)) return;
const marker = document.createComment(`ncore-poster-drop-origin:${drop.id || ''}`);
drop.parentNode.insertBefore(marker, drop);
posterDropOrigins.set(drop, marker);
}
function findPosterRowContext(torrentId) {
const id = String(torrentId || '');
if (!id) return null;
for (const row of torrentContainer.querySelectorAll(':scope > .box_torrent')) {
const detailsLink = getDetailsLink(row);
if (detailsLink && getTorrentId(detailsLink.href) === id) {
return { row, detailsLink };
}
}
return null;
}
function ensurePosterModal() {
if (posterModalElements) return posterModalElements;
const overlay = document.createElement('div');
overlay.id = 'ncore-poster-details-overlay';
overlay.setAttribute('aria-hidden', 'true');
const modal = document.createElement('div');
modal.id = 'ncore-poster-details-modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'ncore-poster-details-title');
const header = document.createElement('div');
header.id = 'ncore-poster-details-header';
const title = document.createElement('div');
title.id = 'ncore-poster-details-title';
const openDetails = document.createElement('a');
openDetails.id = 'ncore-poster-details-open';
openDetails.target = '_blank';
openDetails.rel = 'noopener noreferrer';
openDetails.textContent = 'Részletek új lapon';
const close = document.createElement('button');
close.id = 'ncore-poster-details-close';
close.type = 'button';
close.title = 'Bezárás';
close.setAttribute('aria-label', 'Bezárás');
close.textContent = '×';
const content = document.createElement('div');
content.id = 'ncore-poster-details-content';
header.append(title, openDetails, close);
modal.append(header, content);
overlay.appendChild(modal);
document.body.appendChild(overlay);
close.addEventListener('click', () => closeActivePosterModal(true));
overlay.addEventListener('click', event => {
if (event.target === overlay) closeActivePosterModal(true);
});
document.addEventListener('keydown', event => {
if (event.key !== 'Escape' || !posterModalState) return;
if (isFancyBoxActive()) return;
closeActivePosterModal(true);
}, true);
posterModalElements = { overlay, modal, title, openDetails, close, content };
return posterModalElements;
}
function restorePosterDrop(drop) {
if (!drop) return;
const marker = posterDropOrigins.get(drop);
if (marker?.parentNode) marker.parentNode.insertBefore(drop, marker.nextSibling);
drop.classList.remove('ncore-poster-drop');
const torrentId = String(drop.id || '');
const context = findPosterRowContext(torrentId);
context?.row?.classList.remove('ncore-poster-expanded');
}
function hidePosterModal() {
if (!posterModalElements) return;
posterModalElements.overlay.classList.remove('is-open');
posterModalElements.overlay.setAttribute('aria-hidden', 'true');
document.body.classList.remove('ncore-poster-modal-open');
}
function showPosterModal(row, detailsLink, drop) {
if (!row || !detailsLink || !drop) return;
const modal = ensurePosterModal();
rememberPosterDropOrigin(drop);
const torrentTitle = normalizeText(detailsLink.getAttribute('title') || detailsLink.textContent) || 'Torrent részletek';
const detailsUrl = detailsLink.getAttribute('href') || detailsLink.href || '#';
drop.classList.add('ncore-poster-drop');
modal.content.replaceChildren(drop);
modal.content.scrollTop = 0;
modal.title.textContent = torrentTitle;
modal.openDetails.href = detailsUrl;
modal.overlay.classList.add('is-open');
modal.overlay.setAttribute('aria-hidden', 'false');
document.body.classList.add('ncore-poster-modal-open');
row.classList.add('ncore-poster-expanded');
posterModalState = {
drop,
row,
detailsLink,
torrentId: String(drop.id || ''),
};
modal.close.focus({ preventScroll: true });
}
function restoreAllPosterDrops() {
posterModalRequestId++;
hidePosterModal();
posterModalState = null;
for (const drop of posterDropOrigins.keys()) restorePosterDrop(drop);
}
function isPosterDropOpen(drop) {
return Boolean(drop) && drop.style.display !== 'none';
}
function invokeNativeTorrentToggle(detailsLink, torrentId) {
const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
if (typeof pageWindow.torrent === 'function') {
try {
pageWindow.torrent(torrentId);
return;
} catch (error) {
console.debug('[nCore Tools] Natív torrent lenyitás közvetlenül nem sikerült:', error);
}
}
detailsLink.click();
}
function isFancyBoxActive() {
const candidates = document.querySelectorAll('#fancybox-overlay, #fancybox-wrap, .fancybox-container');
return Array.from(candidates).some(element => {
const computed = getComputedStyle(element);
return computed.display !== 'none' &&
computed.visibility !== 'hidden' &&
computed.opacity !== '0';
});
}
function closePosterDrop(drop, closeNative = true) {
if (!drop) return;
const torrentId = String(drop.id || '');
const context = findPosterRowContext(torrentId);
const currentDrop = getPosterDrop(torrentId) || drop;
if (closeNative && isPosterDropOpen(currentDrop)) {
if (context) invokeNativeTorrentToggle(context.detailsLink, torrentId);
if (isPosterDropOpen(currentDrop)) currentDrop.style.display = 'none';
}
if (posterModalState?.torrentId === torrentId) {
hidePosterModal();
posterModalState = null;
}
restorePosterDrop(currentDrop);
if (currentDrop !== drop) restorePosterDrop(drop);
}
function closeActivePosterModal(closeNative = true) {
posterModalRequestId++;
const drop = posterModalState?.drop;
if (!drop) {
hidePosterModal();
return;
}
closePosterDrop(drop, closeNative);
}
function closeOtherPosterDrops(exceptDrop = null) {
const nativeDrops = torrentContainer.querySelectorAll(
':scope > .torrent_lenyilo, :scope > .torrent_lenyilo2'
);
const drops = new Set([
...posterDropOrigins.keys(),
...nativeDrops,
...(posterModalState?.drop ? [posterModalState.drop] : []),
]);
for (const drop of drops) {
if (drop !== exceptDrop && isPosterDropOpen(drop)) closePosterDrop(drop, true);
}
}
function waitForPosterDropOpen(torrentId, fallbackDrop, requestId, timeout = 5000) {
return new Promise(resolve => {
const startedAt = performance.now();
function check() {
if (requestId !== posterModalRequestId) {
resolve(null);
return;
}
const currentDrop = getPosterDrop(torrentId) || fallbackDrop;
if (currentDrop && isPosterDropOpen(currentDrop)) {
resolve(currentDrop);
return;
}
if (performance.now() - startedAt >= timeout) {
resolve(null);
return;
}
requestAnimationFrame(check);
}
check();
});
}
async function togglePosterDrop(row, detailsLink, torrentId) {
const id = String(torrentId || '');
if (!id) return;
const existingDrop = getPosterDrop(id);
if (posterModalState?.torrentId === id && existingDrop && isPosterDropOpen(existingDrop)) {
closeActivePosterModal(true);
return;
}
if (posterModalState) closeActivePosterModal(true);
const requestId = ++posterModalRequestId;
let drop = getPosterDrop(id);
closeOtherPosterDrops(drop);
if (!drop || !isPosterDropOpen(drop)) {
invokeNativeTorrentToggle(detailsLink, id);
}
drop = await waitForPosterDropOpen(id, drop, requestId);
if (requestId !== posterModalRequestId || !drop) return;
showPosterModal(row, detailsLink, drop);
}
function bindPosterDetailsLink(link, row, detailsLink, torrentId) {
if (!link || !torrentId) return;
link.addEventListener('click', event => {
if (!document.body.classList.contains('ncore-poster-view')) return;
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault();
event.stopPropagation();
togglePosterDrop(row, detailsLink, torrentId);
});
}
function adoptOpenPosterDrops() {
posterModalRequestId++;
let adopted = false;
for (const row of torrentContainer.querySelectorAll(':scope > .box_torrent')) {
const detailsLink = getDetailsLink(row);
if (!detailsLink) continue;
const torrentId = getTorrentId(detailsLink.href);
const drop = getPosterDrop(torrentId);
if (!drop || !isPosterDropOpen(drop)) continue;
if (!adopted) {
showPosterModal(row, detailsLink, drop);
adopted = true;
} else {
closePosterDrop(drop, true);
}
}
}
function buildPosterCard(row) {
if (!(row instanceof HTMLElement) || row.querySelector(':scope > .ncore-poster-card')) return;
const detailsLink = getDetailsLink(row);
if (!detailsLink) return;
let detailsUrl = detailsLink.getAttribute('href') || detailsLink.href || '#';
try { detailsUrl = new URL(detailsUrl, window.location.href).href; } catch (_) {}
const titleText = normalizeText(detailsLink.getAttribute('title') || detailsLink.textContent) || 'Torrent';
const qualityLabel = getTorrentQualityLabel(titleText);
const movieTitle = getMovieTitle(row);
const categoryImageSource = row.querySelector('.box_alap_img img.categ_link, img.categ_link');
const categoryAnchorSource = categoryImageSource?.closest('a[href]');
const categoryTitle = normalizeText(
categoryImageSource?.getAttribute('title') ||
categoryImageSource?.getAttribute('alt') ||
''
);
const posterUrl = getPosterUrl(row);
const imdbId = getImdbId(row);
const torrentId = getTorrentId(detailsUrl);
const originalBookmark = row.querySelector('.torrent_konyvjelzo, .torrent_konyvjelzo2');
const plusCount = (row.querySelector('.box_d2')?.textContent.match(/\+/g) || []).length;
const uploaded = normalizePosterUploaded(getCellText(row, ['.box_feltoltve2', '.box_feltoltve']));
const size = getCellText(row, ['.box_meret2', '.box_meret']);
const seed = getCellText(row, ['.box_s2', '.box_s']);
const leech = getCellText(row, ['.box_l2', '.box_l']);
const imdbRating = getImdbRating(row);
const imdbUrl = getImdbUrl(row);
const card = document.createElement('article');
card.className = 'ncore-poster-card';
if (settings.highlight && plusCount >= getHighlightMinPlus()) card.classList.add('ncore-poster-plus');
const media = document.createElement('div');
media.className = 'ncore-poster-media';
const imageLink = document.createElement('a');
imageLink.className = 'ncore-poster-image-link';
imageLink.href = detailsUrl;
imageLink.target = '_blank';
imageLink.rel = 'noopener noreferrer';
imageLink.title = `${movieTitle || titleText} – részletek`;
bindPosterDetailsLink(imageLink, row, detailsLink, torrentId);
if (posterUrl) {
const image = document.createElement('img');
image.className = 'ncore-poster-image';
image.src = posterUrl;
image.alt = movieTitle || titleText;
image.loading = 'lazy';
image.decoding = 'async';
image.addEventListener('error', () => {
const placeholder = document.createElement('div');
placeholder.className = 'ncore-poster-placeholder';
placeholder.textContent = 'Borítókép nem elérhető';
image.replaceWith(placeholder);
}, { once: true });
imageLink.appendChild(image);
} else {
const placeholder = document.createElement('div');
placeholder.className = 'ncore-poster-placeholder';
placeholder.textContent = 'Nincs borítókép';
imageLink.appendChild(placeholder);
}
media.appendChild(imageLink);
if ((categoryImageSource && categoryAnchorSource) || qualityLabel || (imdbRating && imdbUrl)) {
const badges = document.createElement('div');
badges.className = 'ncore-poster-badges';
if (categoryImageSource && categoryAnchorSource) {
const categoryBadge = document.createElement('a');
categoryBadge.className = 'ncore-poster-category-badge';
categoryBadge.href = categoryAnchorSource.getAttribute('href') || categoryAnchorSource.href || '#';
categoryBadge.title = categoryTitle || 'Kategória megnyitása';
categoryBadge.setAttribute('aria-label', categoryBadge.title);
const categoryImage = document.createElement('img');
categoryImage.className = 'ncore-poster-category-image';
categoryImage.src = categoryImageSource.src;
categoryImage.alt = categoryImageSource.getAttribute('alt') || 'Kategória';
categoryBadge.appendChild(categoryImage);
badges.appendChild(categoryBadge);
}
if (qualityLabel) {
const qualityBadge = document.createElement('span');
qualityBadge.className = 'ncore-poster-quality-badge';
qualityBadge.textContent = qualityLabel;
qualityBadge.title = `Minőség / formátum: ${qualityLabel}`;
badges.appendChild(qualityBadge);
}
if (imdbRating && imdbUrl) {
const imdbBadge = document.createElement('a');
imdbBadge.className = 'ncore-poster-imdb-badge';
imdbBadge.href = imdbUrl;
imdbBadge.target = '_blank';
imdbBadge.rel = 'noopener noreferrer';
imdbBadge.textContent = `IMDb ${imdbRating}`;
imdbBadge.title = 'Megnyitás az IMDb-n';
imdbBadge.setAttribute('aria-label', `IMDb értékelés: ${imdbRating}`);
imdbBadge.addEventListener('click', event => event.stopPropagation());
badges.appendChild(imdbBadge);
}
media.appendChild(badges);
}
const body = document.createElement('div');
body.className = 'ncore-poster-body';
const title = document.createElement('a');
title.className = 'ncore-poster-title';
title.href = detailsUrl;
title.target = '_blank';
title.rel = 'noopener noreferrer';
title.textContent = titleText;
title.title = titleText;
bindPosterDetailsLink(title, row, detailsLink, torrentId);
body.appendChild(title);
if (movieTitle) {
const movieTitleElement = document.createElement('div');
movieTitleElement.className = 'ncore-poster-movie-title';
movieTitleElement.textContent = movieTitle;
movieTitleElement.title = movieTitle;
body.appendChild(movieTitleElement);
}
const meta = document.createElement('div');
meta.className = 'ncore-poster-meta';
const metaItems = [
createMetaSpan('', size, size ? 'Méret' : ''),
createMetaSpan('seed', seed ? `S: ${seed}` : '', 'Seed'),
createMetaSpan('leech', leech ? `L: ${leech}` : '', 'Leech'),
createMetaSpan('', uploaded, uploaded ? 'Feltöltve' : ''),
].filter(Boolean);
meta.append(...metaItems);
if (metaItems.length) body.appendChild(meta);
const actions = document.createElement('div');
actions.className = 'ncore-poster-actions';
if (torrentId) {
const downloadUrl = getTorrentDownloadUrl(row, torrentId);
if (downloadUrl) {
const downloadButton = document.createElement('button');
downloadButton.type = 'button';
downloadButton.className = 'ncore-poster-action';
downloadButton.appendChild(createLucideIcon('download'));
downloadButton.title = 'Torrent fájl letöltése';
downloadButton.setAttribute('aria-label', downloadButton.title);
downloadButton.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
window.location.href = downloadUrl;
});
actions.appendChild(downloadButton);
}
}
if (originalBookmark) {
const bookmarkButton = document.createElement('button');
bookmarkButton.type = 'button';
bookmarkButton.className = 'ncore-poster-action';
setupPosterBookmarkButton(bookmarkButton);
bookmarkButton.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
try {
originalBookmark.click();
} catch (_) {
const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
if (typeof pageWindow.konyvjelzo === 'function' && torrentId) {
pageWindow.konyvjelzo(torrentId);
}
}
});
actions.appendChild(bookmarkButton);
}
if (settings.qbittorrent && torrentId) {
const qbButton = document.createElement('button');
qbButton.type = 'button';
qbButton.className = 'ncore-poster-action qb';
qbButton.textContent = 'qB';
qbButton.title = 'Küldés qBittorrentbe';
qbButton.setAttribute('aria-label', qbButton.title);
qbButton.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
const dlUrl = getTorrentDownloadUrl(row, torrentId);
if (!dlUrl) {
showToast('Nem található a torrent letöltési linkje.', 'error');
return;
}
sendToQB(dlUrl, {
torrentId,
title: titleText,
});
});
actions.appendChild(qbButton);
}
if (settings.seen && imdbId && !isSeries(row)) {
const seenButton = document.createElement('button');
seenButton.type = 'button';
seenButton.className = 'ncore-poster-action seen';
seenButton.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
const nextSeen = !seenSync.isSeen(imdbId);
seenSync.setSeen(imdbId, nextSeen, getSeenMovieTitle(row) || titleText);
document.querySelectorAll('.box_torrent').forEach(otherRow => {
const otherImdb = getImdbId(otherRow);
if (otherImdb === imdbId) {
otherRow.classList.toggle('ncore-seen', nextSeen);
const otherButton = otherRow.querySelector(':scope > .ncore-poster-card .ncore-poster-action[data-seen-button="1"]');
if (otherButton) updatePosterSeenState(otherRow, otherButton, imdbId);
}
});
showToast(nextSeen ? 'Film megjelölve látottként.' : 'Látott jelölés visszavonva.');
});
seenButton.dataset.seenButton = '1';
updatePosterSeenState(row, seenButton, imdbId);
actions.appendChild(seenButton);
}
body.appendChild(actions);
card.append(media, body);
row.appendChild(card);
}
function buildAllPosterCards(root = document) {
if (root instanceof HTMLElement && root.matches('.box_torrent')) buildPosterCard(root);
root.querySelectorAll?.('.box_torrent').forEach(buildPosterCard);
}
function refreshPosterSeenStates(root = document) {
const rows = [];
if (root instanceof HTMLElement && root.matches('.box_torrent')) rows.push(root);
root.querySelectorAll?.('.box_torrent').forEach(row => rows.push(row));
for (const row of rows) {
const imdbId = getImdbId(row);
const button = row.querySelector(':scope > .ncore-poster-card .ncore-poster-action[data-seen-button="1"]');
if (button && imdbId) updatePosterSeenState(row, button, imdbId);
}
}
const switcher = document.createElement('div');
switcher.id = 'ncore-torrent-view-switcher';
switcher.setAttribute('role', 'group');
switcher.setAttribute('aria-label', 'Torrentlista nézet');
const listButton = document.createElement('button');
listButton.type = 'button';
listButton.className = 'ncore-torrent-view-button';
listButton.dataset.view = VIEW_LIST;
listButton.title = 'Lista nézet';
listButton.setAttribute('aria-label', 'Lista nézet');
const listLabel = document.createElement('span');
listLabel.className = 'ncore-torrent-view-label';
listLabel.textContent = 'Lista';
listButton.append(createLucideIcon('text-align-justify'), listLabel);
const posterButton = document.createElement('button');
posterButton.type = 'button';
posterButton.className = 'ncore-torrent-view-button';
posterButton.dataset.view = VIEW_POSTER;
posterButton.title = 'Poszter nézet';
posterButton.setAttribute('aria-label', 'Poszter nézet');
const posterLabel = document.createElement('span');
posterLabel.className = 'ncore-torrent-view-label';
posterLabel.textContent = 'Poszter';
posterButton.append(createLucideIcon('layout-grid'), posterLabel);
switcher.append(listButton, posterButton);
const switcherHost = document.createElement('div');
switcherHost.className = 'ncore-torrent-view-switcher-host';
listaAll.parentNode.insertBefore(switcherHost, listaAll);
switcherHost.appendChild(switcher);
function positionViewSwitcher() {
switcherHost.style.width = `${listaAll.offsetWidth}px`;
const hostRect = switcherHost.getBoundingClientRect();
const headerRect = listaHeader.getBoundingClientRect();
const headerCenter = headerRect.top - hostRect.top + (headerRect.height / 2);
switcher.style.top = `${headerCenter}px`;
}
positionViewSwitcher();
window.addEventListener('resize', positionViewSwitcher);
async function setView(view, persist = true) {
if (!VALID_VIEWS.has(view)) view = VIEW_LIST;
currentView = view;
if (view === VIEW_POSTER) {
buildAllPosterCards(torrentContainer);
refreshPosterSeenStates(torrentContainer);
document.body.classList.add('ncore-poster-view');
adoptOpenPosterDrops();
} else {
restoreAllPosterDrops();
document.body.classList.remove('ncore-poster-view');
}
switcher.querySelectorAll('.ncore-torrent-view-button').forEach(button => {
button.setAttribute('aria-pressed', button.dataset.view === view ? 'true' : 'false');
});
if (persist) await gmSet(GM_KEY_TORRENT_VIEW, view);
}
listButton.addEventListener('click', () => setView(VIEW_LIST));
posterButton.addEventListener('click', () => setView(VIEW_POSTER));
buildAllPosterCards(torrentContainer);
await setView(currentView, false);
new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
buildAllPosterCards(node);
}
}
}).observe(torrentContainer, { childList: true, subtree: true });
}
// -------------------------------------------------------------------------
// 7) Népszerű torrentek kiemelése
// -------------------------------------------------------------------------
function initHighlight() {
if (!location.pathname.endsWith('/torrents.php')) return;
const HIGHLIGHT_BG = '#600A0A';
const HIGHLIGHT_HOVER_BG = '#8A1515';
const minPlus = getHighlightMinPlus();
const style = document.createElement('style');
style.textContent = `
.ncore-plus-highlight { background: linear-gradient(${HIGHLIGHT_BG}, ${HIGHLIGHT_BG}); }
.ncore-plus-highlight:hover { background: linear-gradient(${HIGHLIGHT_HOVER_BG}, ${HIGHLIGHT_HOVER_BG}); }
`;
document.head.appendChild(style);
function highlight(box) {
const plusEl = box.querySelector('.box_d2');
if (!plusEl) return;
const plusCount = (plusEl.textContent.match(/\+/g) || []).length;
const main = box.querySelector('.box_nagy, .box_nagy2');
if (!main) return;
main.classList.toggle('ncore-plus-highlight', plusCount >= minPlus);
}
document.querySelectorAll('.box_torrent').forEach(highlight);
new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
if (node.matches('.box_torrent')) highlight(node);
node.querySelectorAll?.('.box_torrent').forEach(highlight);
}
}
}).observe(document.body, { childList: true, subtree: true });
}
// -------------------------------------------------------------------------
// 8) qBittorrent integráció
// -------------------------------------------------------------------------
function initQBittorrent() {
if (!location.pathname.endsWith('/torrents.php')) return;
function normalizeTorrentTitle(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function findTorrentDetailsLink(torrentId, root = document) {
const id = String(torrentId || '').trim();
if (!id) return null;
const links = root.querySelectorAll?.(
'.torrent_txt > a[href*="action=details"], ' +
'.torrent_txt2 > a[href*="action=details"]'
) || [];
for (const link of links) {
try {
const url = new URL(link.getAttribute('href'), window.location.origin);
if (url.searchParams.get('action') === 'details' && url.searchParams.get('id') === id) {
return link;
}
} catch (_) {}
}
return null;
}
function getTorrentTitle(torrentId, downloadLink = null) {
const localRow = downloadLink?.closest('.box_torrent');
const localLink = localRow ? findTorrentDetailsLink(torrentId, localRow) : null;
const torrentLink = localLink || findTorrentDetailsLink(torrentId);
return normalizeTorrentTitle(torrentLink?.getAttribute('title') || torrentLink?.textContent);
}
function injectQBLink(rootEl) {
if (!rootEl) return;
const selector = '.letoltve_txt a[href*="torrents.php?action=download"]';
const downloadLinks = [];
if (rootEl.matches?.(selector)) downloadLinks.push(rootEl);
rootEl.querySelectorAll?.(selector).forEach(link => downloadLinks.push(link));
for (const dlA of downloadLinks) {
const parent = dlA.parentNode;
if (!parent) continue;
const dlUrl = new URL(dlA.getAttribute('href'), window.location.origin).href;
const torrentId = new URL(dlUrl).searchParams.get('id') || '';
const torrentTitle = getTorrentTitle(torrentId, dlA);
const alreadyInjected = Array.from(parent.querySelectorAll?.('.qb-inline-link') || [])
.some(link => link.dataset.downloadUrl === dlUrl);
if (alreadyInjected) continue;
const sep = document.createElement('span');
sep.className = 'qb-inline-separator';
sep.textContent = ' | ';
const qbA = document.createElement('a');
qbA.href = 'javascript:void(0);';
qbA.textContent = 'qBittorrent';
qbA.className = 'qb-inline-link';
qbA.dataset.downloadUrl = dlUrl;
qbA.addEventListener('click', () => sendToQB(dlUrl, {
torrentId,
title: torrentTitle,
}));
dlA.parentNode.insertBefore(sep, dlA.nextSibling);
dlA.parentNode.insertBefore(qbA, sep.nextSibling);
}
}
// Fix search form method to GET.
const form = document.getElementById('kereses_mezo');
if (form) form.method = 'GET';
// Részletező oldal.
const alt = $('link[rel="alternate"]');
const key = alt?.getAttribute('href')?.slice(-32) || '';
if (window.location.search.includes('action=details')) {
const params = new URLSearchParams(window.location.search);
const torrentId = params.get('id');
const container = document.querySelector('.torrent_reszletek_konyvjelzo');
if (torrentId && container && !container.querySelector('.qbittorrent-add-btn')) {
const dlUrl = `${window.location.origin}/torrents.php?action=download&id=${torrentId}&key=${key}`;
const qbLink = document.createElement('a');
qbLink.style.fontWeight = 'normal';
qbLink.href = 'javascript:void(0);';
qbLink.className = 'qbittorrent-add-btn';
qbLink.title = 'Küldés qBittorrentbe';
qbLink.textContent = '[qBittorrent]';
qbLink.addEventListener('click', () => sendToQB(dlUrl, {
torrentId,
title: normalizeTorrentTitle(document.querySelector('.torrent_reszletek_cim')?.textContent),
}));
container.appendChild(qbLink);
}
}
new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
injectQBLink(node);
node.querySelectorAll?.('.torrent_lenyilo, .torrent_lenyilo_tartalom, .torrent_lenyilo_lehetoseg, .torrent_lenyilo_lab, .torrent_lenyilo_tartalom *').forEach(injectQBLink);
}
}
}).observe(document.documentElement, { childList: true, subtree: true });
injectQBLink(document);
}
// -------------------------------------------------------------------------
// Init
// -------------------------------------------------------------------------
if (settings.seen || settings.qbittorrent) await seenSync.init();
if (settings.qbittorrent) initQBittorrent();
await initTorrentViewSwitcher();
if (settings.highlight) initHighlight();
if (settings.seen) initSeen();
if (settings.embedImages) initEmbedImages();
if (settings.removeAds) initRemoveAds();
if (settings.noThanks) initNoThanks();
if (settings.dedereferer) initDedereferer();
ensureInfosavLinks();
const infosav = document.querySelector('#infosav');
if (infosav) {
new MutationObserver(() => ensureInfosavLinks())
.observe(infosav, { childList: true, subtree: true });
}
})();