// ==UserScript== // @name RIN Mobile Thread // @description Mobile Beitragsansicht fuer cs.rin.ru // @version 2.3 // @match *://cs.rin.ru/forum/viewtopic.php* // @run-at document-start // ==/UserScript== (function () { 'use strict'; var DEBUG = true; var log = []; function h() { return document.head || document.documentElement; } if (!document.querySelector('meta[name="viewport"][data-rm]')) { var m = document.createElement('meta'); m.name = 'viewport'; m.content = 'width=device-width, initial-scale=1, viewport-fit=cover'; m.setAttribute('data-rm', '1'); h().appendChild(m); } var st = document.createElement('style'); st.textContent = ` html, body { max-width:100% !important; overflow-x:hidden !important; margin:0 !important; padding:0 !important; } body { font-size:16px !important; -webkit-text-size-adjust:100% !important; } .rm-dbg { background:#222; color:#0f0; font:12px/1.45 monospace; padding:8px 10px; white-space:pre-wrap; } .rm-hidden { display:none !important; } .rm-root { box-sizing:border-box !important; display:block !important; width:100vw !important; max-width:100vw !important; margin:0 !important; padding:0 0 64px 0 !important; overflow:hidden !important; } .rm-topic { box-sizing:border-box; padding:16px 14px 12px; font-size:21px; font-weight:650; line-height:1.3; overflow-wrap:break-word; } .rm-post { box-sizing:border-box !important; width:100% !important; max-width:100% !important; padding:14px !important; border-top:1px solid rgba(128,128,128,.25) !important; } .rm-head { display:flex; align-items:center; gap:9px; margin-bottom:10px; min-width:0; } .rm-av { flex:none; width:28px; height:28px; border-radius:50%; object-fit:cover; background:rgba(128,128,128,.25); } .rm-who { min-width:0; flex:1; } .rm-user { font-size:14px; font-weight:650; line-height:1.2; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .rm-sub { font-size:11px; opacity:.55; line-height:1.3; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .rm-body { font-size:17px !important; line-height:1.55 !important; overflow-wrap:break-word !important; word-break:normal !important; } .rm-body * { max-width:100% !important; } .rm-body img { height:auto !important; border-radius:6px; } .rm-body a { overflow-wrap:anywhere !important; } .rm-body table { display:block !important; overflow-x:auto !important; width:auto !important; } .rm-body pre, .rm-body code { display:block; overflow-x:auto; white-space:pre; font-size:13px; padding:10px; border-radius:8px; background:rgba(128,128,128,.14); } .rm-body blockquote, .rm-body .quote, .rm-body table.quote { display:block !important; margin:12px 0 !important; padding:8px 0 8px 12px !important; border-left:3px solid rgba(140,140,255,.5) !important; font-size:15px !important; opacity:.8 !important; background:none !important; } .rm-body ul, .rm-body ol { padding-left:22px !important; } .rm-body hr { border:0; border-top:1px solid rgba(128,128,128,.3); } .rm-sig { margin-top:12px; font-size:13px; opacity:.5; } .rm-sig summary { cursor:pointer; opacity:.8; } .rm-nav { position:fixed; left:0; right:0; bottom:0; z-index:9999; display:flex; align-items:center; justify-content:space-between; gap:8px; padding:10px 12px calc(10px + env(safe-area-inset-bottom)); background:rgba(28,28,30,.94); -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px); border-top:1px solid rgba(128,128,128,.3); } .rm-nav a, .rm-nav span { font-size:14px; color:#eee !important; text-decoration:none !important; white-space:nowrap; } .rm-nav a { padding:7px 13px; border-radius:99px; border:1px solid rgba(160,160,160,.45); } .rm-nav .rm-step { min-width:22px; text-align:center; font-size:17px; line-height:1; padding:9px 12px; } .rm-nav .rm-pos { opacity:.6; font-size:13px; } .rm-nav .rm-pos-btn { opacity:1; padding:7px 12px; border-radius:99px; border:1px solid rgba(160,160,160,.45); cursor:pointer; } .rm-nav .rm-off { opacity:.28; border-color:rgba(160,160,160,.2); } .rm-sheet-bg { position:fixed; top:0; left:0; right:0; bottom:0; z-index:10000; background:rgba(0,0,0,.5); opacity:0; pointer-events:none; transition:opacity .18s; } .rm-sheet-bg.rm-open { opacity:1; pointer-events:auto; } .rm-sheet { box-sizing:border-box; position:fixed; left:0; right:0; bottom:0; z-index:10001; display:flex; flex-direction:column; max-height:72vh; padding:8px 12px calc(12px + env(safe-area-inset-bottom)); color:#eee; background:rgba(28,28,30,.98); -webkit-backdrop-filter:blur(14px); backdrop-filter:blur(14px); border-top:1px solid rgba(128,128,128,.3); border-radius:14px 14px 0 0; transform:translateY(102%); transition:transform .22s ease; } .rm-sheet.rm-open { transform:translateY(0); } .rm-grip { flex:none; width:36px; height:4px; margin:2px auto 12px; border-radius:2px; background:rgba(200,200,200,.35); } .rm-goto { flex:none; display:flex; gap:8px; margin-bottom:12px; } /* 16px verhindert, dass iOS beim Fokussieren in das Feld hineinzoomt. */ .rm-goto input { flex:1; min-width:0; box-sizing:border-box; font-size:16px; padding:9px 12px; border-radius:10px; color:#eee; background:rgba(120,120,120,.18); border:1px solid rgba(160,160,160,.35); } .rm-goto button { flex:none; font-size:14px; padding:9px 18px; border-radius:10px; color:#eee; background:rgba(120,120,120,.18); border:1px solid rgba(160,160,160,.45); } .rm-grid { flex:1; min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch; display:grid; gap:8px; padding-bottom:4px; grid-template-columns:repeat(auto-fill, minmax(56px, 1fr)); } .rm-grid a { display:flex; align-items:center; justify-content:center; min-height:42px; font-size:14px; border-radius:10px; color:#eee !important; text-decoration:none !important; background:rgba(120,120,120,.12); border:1px solid rgba(160,160,160,.3); } .rm-grid a.rm-cur { font-weight:700; background:rgba(120,170,255,.22); border-color:rgba(120,170,255,.9); } `; h().appendChild(st); function txt(el) { return (el ? el.textContent || '' : '') .replace(/\s+/g, ' ').trim(); } // Der Beitragstext liegt in einer verschachtelten Tabelle. closest('tr') // trifft deshalb die innere Zeile, der Autor- und Profilspalte fehlen. // Aufsteigen, bis die Zeile eine Profil- oder Autorenspalte enthaelt. function postRow(pb) { var tr = pb.closest('tr'); while (tr) { if (tr.querySelector(':scope > td.profile') || tr.querySelector(':scope > td .postauthor')) return tr; var up = tr.parentElement ? tr.parentElement.closest('tr') : null; if (!up) return tr; tr = up; } return null; } // --- Seitenzustand --- // Der Block wird aus dem Original-DOM gelesen, bevor es ausgeblendet wird. // phpBB zeigt nie alle Seitenzahlen an, deshalb wird nicht auf gerenderte // Links gesetzt, sondern jede Ziel-URL aus start = (Seite - 1) * per_page // selbst gebaut. function pageState() { var s = { cur: 1, total: 1, per: 0, prev: null, next: null }; // Der Paginierungsblock ist der einzige Ort, an dem Ziffern-Links // Seitenzahlen bedeuten. Beitragstexte enthalten ebenfalls Links mit // start=, die hier nicht mitgezaehlt werden duerfen. var box = null; var jumpA = document.querySelector('a[onclick*="jumpto"]'); if (jumpA) box = jumpA.parentElement; if (!box) { document.querySelectorAll('a[href*="start="]').forEach(function (a) { if (!box && /^(Next|Weiter|Nächste)$/i.test(txt(a))) box = a.parentElement; }); } // per_page steht als Literal in der jumpto()-Funktion der Seite. document.querySelectorAll('script').forEach(function (el) { if (s.per) return; var m = (el.textContent || '').match(/per_page\s*=\s*'?(\d+)/); if (m) s.per = parseInt(m[1], 10); }); if (box) { box.querySelectorAll('a[href*="start="]').forEach(function (a) { var t = txt(a); if (!/^\d+$/.test(t)) return; var n = parseInt(t, 10); if (n > s.total) s.total = n; var ms = a.href.match(/[?&]start=(\d+)/); if (!s.per && ms && n > 1) { var per = parseInt(ms[1], 10) / (n - 1); if (per > 0 && per === Math.floor(per)) s.per = per; } }); } var cs = location.search.match(/[?&]start=(\d+)/); if (s.per) s.cur = Math.floor((cs ? parseInt(cs[1], 10) : 0) / s.per) + 1; else if (box) { var strong = box.querySelector('strong, b > strong'); if (strong && /^\d+$/.test(txt(strong))) s.cur = parseInt(txt(strong), 10); } // Aeltere Themes geben stattdessen "Page X of Y" aus. var pos = txt(document.body).match(/Page\s+(\d+)\s+of\s+(\d+)/i); if (pos) { if (!s.per) s.cur = parseInt(pos[1], 10); s.total = Math.max(s.total, parseInt(pos[2], 10)); } if (s.cur > s.total) s.total = s.cur; s.url = function (n) { if (n === s.cur || n < 1 || n > s.total) return null; if (n !== 1 && !s.per) return null; var u = location.href.split('#')[0]; var start = (n - 1) * s.per; if (/[?&]start=\d+/.test(u)) return u.replace(/([?&]start=)\d+/, '$1' + start); return u + (u.indexOf('?') === -1 ? '?' : '&') + 'start=' + start; }; s.prev = s.url(s.cur - 1); s.next = s.url(s.cur + 1); // Ohne bekanntes per_page bleibt nur, die Original-Links zu uebernehmen. if (!s.per) { document.querySelectorAll('a').forEach(function (a) { var t = txt(a); if (!s.prev && /^(Previous|Vorherige)$/i.test(t)) s.prev = a.href; if (!s.next && /^(Next|Weiter|Nächste)$/i.test(t)) s.next = a.href; }); } log.push('page=' + s.cur + '/' + s.total + ' per=' + s.per); return s; } // Seitenwahl als Sheet ueber der Leiste. Wird erst beim ersten Antippen // gebaut: bei 1000+ Seiten waere das Raster sonst umsonst im DOM. function pageSheet(s) { var bg = null, sheet = null; function go(n) { n = Math.min(Math.max(n, 1), s.total); var u = s.url(n); if (u) location.href = u; else close(); } function close() { if (!bg) return; bg.classList.remove('rm-open'); sheet.classList.remove('rm-open'); document.body.style.overflow = ''; } function make() { bg = document.createElement('div'); bg.className = 'rm-sheet-bg'; bg.addEventListener('click', close); sheet = document.createElement('div'); sheet.className = 'rm-sheet'; var grip = document.createElement('div'); grip.className = 'rm-grip'; sheet.appendChild(grip); var row = document.createElement('div'); row.className = 'rm-goto'; var inp = document.createElement('input'); inp.type = 'text'; inp.inputMode = 'numeric'; inp.pattern = '[0-9]*'; inp.placeholder = 'Seite 1 – ' + s.total; inp.setAttribute('aria-label', 'Seitenzahl'); var btn = document.createElement('button'); btn.type = 'button'; btn.textContent = 'Los'; function submit() { var n = parseInt(inp.value, 10); if (n > 0) go(n); } btn.addEventListener('click', submit); inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); submit(); } }); row.appendChild(inp); row.appendChild(btn); sheet.appendChild(row); var grid = document.createElement('div'); grid.className = 'rm-grid'; var curEl = null; for (var i = 1; i <= s.total; i++) { var a = document.createElement('a'); a.textContent = i; if (i === s.cur) { a.className = 'rm-cur'; curEl = a; } else { var u = s.url(i); if (u) a.href = u; } grid.appendChild(a); } sheet.appendChild(grid); document.body.appendChild(bg); document.body.appendChild(sheet); return curEl; } return function open() { var curEl = bg ? null : make(); document.body.style.overflow = 'hidden'; // Ein Frame Abstand, sonst faellt der Uebergang beim ersten Oeffnen aus. requestAnimationFrame(function () { bg.classList.add('rm-open'); sheet.classList.add('rm-open'); if (curEl) curEl.scrollIntoView({ block: 'center' }); }); }; } function build() { var bodies = document.querySelectorAll('span.postbody, .postbody'); log.push('postbodies=' + bodies.length); if (!bodies.length) return; var root = document.createElement('div'); root.className = 'rm-root'; var tt = document.querySelector('a.maintitle, h1, .maintitle'); if (tt) { var th = document.createElement('div'); th.className = 'rm-topic'; th.textContent = txt(tt); root.appendChild(th); } var built = 0, seen = new Set(); bodies.forEach(function (pb) { var row = postRow(pb); if (!row || seen.has(row)) return; seen.add(row); var card = document.createElement('div'); card.className = 'rm-post'; // --- Zellen dieser Zeile + der Kopfzeilen darueber --- var cells = []; Array.prototype.forEach.call(row.children, function (td) { if (!td.contains(pb)) cells.push(td); }); var pr = row.previousElementSibling, g = 0; while (pr && g++ < 3) { Array.prototype.forEach.call(pr.children, function (td) { cells.push(td); }); if (pr.querySelector('.postauthor, a[href*="profile.php"]')) break; pr = pr.previousElementSibling; } // --- Username --- var user = ''; for (var i = 0; i < cells.length && !user; i++) { var ua = cells[i].querySelector( '.postauthor, .username-coloured, a[href*="profile.php"]'); if (ua) user = txt(ua); } if (!user) { for (var i2 = 0; i2 < cells.length && !user; i2++) { var nm = cells[i2].querySelector('span.name b, span.name, b.name'); if (nm) user = txt(nm); } } user = user.replace(/^Post subject:.*/i, '').trim(); // --- Avatar --- var avImg = null, bestArea = 0; cells.forEach(function (td) { td.querySelectorAll('img').forEach(function (im) { var s = im.getAttribute('src') || ''; if (/spacer|blank|icon|rank|star|folder|button|pm|email|quote|edit|report|www|msn|aim|yim|icq|topic/i.test(s)) return; if (/avatar/i.test(s)) { avImg = im; bestArea = 1e9; return; } var w = parseInt(im.getAttribute('width'), 10) || im.naturalWidth || 0; var hh = parseInt(im.getAttribute('height'), 10) || im.naturalHeight || 0; var a = w * hh; if (w >= 40 && a > bestArea) { avImg = im; bestArea = a; } }); }); // --- Rang, Postzahl, Zeitstempel --- var allTxt = cells.map(txt).join(' '); var rank = ''; var rr = allTxt.match(/\b(Administrator|Site Admin|Global Moderator|Moderator|VIP|Uploader|Supporter|Member|Newbie|Guest)\b/i); if (rr) rank = rr[1]; var pc = allTxt.match(/Posts:\s*([\d.,]+)/i); if (pc) rank = (rank ? rank + ' · ' : '') + pc[1] + ' Posts'; var when = ''; var wm = allTxt.match(/Posted:\s*([^|]+?)(?:\s{2,}|Post subject|$)/i); if (wm) when = wm[1].trim(); // --- Kopfzeile --- var head = document.createElement('div'); head.className = 'rm-head'; var av; if (avImg) { av = document.createElement('img'); av.src = avImg.src; } else { av = document.createElement('div'); av.textContent = (user || '?').charAt(0).toUpperCase(); av.style.cssText = 'display:flex;align-items:center;' + 'justify-content:center;font-size:13px;font-weight:700;' + 'color:rgba(255,255,255,.75);'; } av.className = 'rm-av'; head.appendChild(av); var who = document.createElement('div'); who.className = 'rm-who'; var un = document.createElement('div'); un.className = 'rm-user'; un.textContent = user || 'Gast'; who.appendChild(un); var sub = document.createElement('div'); sub.className = 'rm-sub'; sub.textContent = [rank, when].filter(Boolean).join(' · '); who.appendChild(sub); head.appendChild(who); card.appendChild(head); // --- Signatur abtrennen --- var sig = null, hr = pb.querySelector('hr'); if (hr) { sig = document.createElement('details'); sig.className = 'rm-sig'; var sm = document.createElement('summary'); sm.textContent = 'Signatur'; sig.appendChild(sm); var n = hr.nextSibling; while (n) { var nx = n.nextSibling; sig.appendChild(n); n = nx; } hr.remove(); } var bodyWrap = document.createElement('div'); bodyWrap.className = 'rm-body'; bodyWrap.appendChild(pb); card.appendChild(bodyWrap); if (sig && sig.childNodes.length > 1) card.appendChild(sig); root.appendChild(card); built++; }); log.push('posts=' + built); if (!built) return; // --- Navigationsleiste --- var pg = pageState(); var nav = document.createElement('div'); nav.className = 'rm-nav'; function navBtn(href, label, aria) { var e = document.createElement('a'); e.className = 'rm-step' + (href ? '' : ' rm-off'); e.textContent = label; e.setAttribute('aria-label', aria); if (href) e.href = href; return e; } nav.appendChild(navBtn(pg.url(1), '«', 'Erste Seite')); nav.appendChild(navBtn(pg.prev, '‹', 'Vorherige Seite')); var pos = document.createElement('span'); pos.className = 'rm-pos'; pos.textContent = pg.cur + ' / ' + pg.total; // Ohne bekanntes per_page laesst sich kein Sprungziel bauen, dann bleibt // die Anzeige reine Beschriftung. if (pg.total > 1 && pg.per) { pos.className += ' rm-pos-btn'; pos.textContent += ' ▾'; pos.setAttribute('role', 'button'); pos.setAttribute('aria-label', 'Seite wählen'); pos.addEventListener('click', pageSheet(pg)); } nav.appendChild(pos); nav.appendChild(navBtn(pg.next, '›', 'Nächste Seite')); nav.appendChild(navBtn(pg.url(pg.total), '»', 'Letzte Seite')); root.appendChild(nav); Array.prototype.forEach.call(document.body.children, function (el) { if (el !== root) el.classList.add('rm-hidden'); }); document.body.appendChild(root); } function run() { try { build(); } catch (e) { log.push('build: ' + e.message); } if (DEBUG) { var d = document.createElement('div'); d.className = 'rm-dbg'; d.textContent = 'RIN Thread v2.3\n' + log.join('\n'); document.body.insertBefore(d, document.body.firstChild); } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run); else run(); })();