// ==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 }); } })();