// ==UserScript== // @name RIN Mobile // @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom // @version 3.1 // @match *://cs.rin.ru/forum/* // @run-at document-start // ==/UserScript== (function () { 'use strict'; var DEBUG = false; var log = []; function h() { return document.head || document.documentElement; } function txt(el) { return (el ? el.textContent || '' : '') .replace(/\s+/g, ' ').trim(); } // Der Seitentyp entscheidet, welche Ansicht gebaut wird. Beide Ansichten // liegen in einem Script, weil sie sich Grundstile und Klassennamen teilen. // Als getrennte Scripts waren auf Threadseiten beide Stylesheets aktiv und // die Ladereihenfolge entschied bei gleich benannten Regeln ueber das // Layout. var here = location.pathname + location.search; var view = /viewtopic\.php/.test(here) ? 'thread' : /viewforum\.php|search\.php/.test(here) || location.pathname === '/forum/' ? 'liste' : ''; log.push('ansicht=' + (view || 'keine')); if (view && !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); } if (view) { var st = document.createElement('style'); st.textContent = ` /* === Grundlage, beide Ansichten === */ 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; } img { max-width:100% !important; height:auto !important; } .rm-dbg { background:#222; color:#0f0; font:12px/1.45 monospace; padding:8px 10px; white-space:pre-wrap; border-bottom:2px solid #0f0; } .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 !important; overflow:hidden !important; } /* Freihaltung hinter der fixierten Leiste der Threadansicht. */ .rm-root.rm-thread { padding-bottom:64px !important; } /* Brotkrumen, beide Ansichten. Fixiert statt sticky: .rm-root traegt overflow:hidden und wuerde als Scrollport einer sticky-Leiste dienen, die darin nie klebt. Fixierte Nachfahren klammert es nicht, weil der Root keinen eigenen Containing Block aufspannt. */ .rm-crumbs { box-sizing:border-box; position:fixed; top:0; left:0; right:0; z-index:9998; display:flex; align-items:center; gap:6px; height:calc(44px + env(safe-area-inset-top)); padding:env(safe-area-inset-top) 12px 0; overflow-x:auto; overflow-y:hidden; -webkit-overflow-scrolling:touch; white-space:nowrap; background:rgba(28,28,30,.94); -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px); border-bottom:1px solid rgba(128,128,128,.3); } .rm-crumbs a { flex:none; padding:8px 2px; font-size:14px; color:#eee !important; text-decoration:none !important; } .rm-crumbs a:last-of-type { font-weight:650; } .rm-crumbs .rm-sep { flex:none; font-size:13px; color:#eee; opacity:.45; } /* Nur gesetzt, wenn die Leiste wirklich gebaut wurde — ohne Brotkrumen im Original-DOM bleibt der Platz sonst leer reserviert. */ body.rm-crumbed .rm-root { padding-top:calc(45px + env(safe-area-inset-top)) !important; } body.rm-crumbed .rm-dbg { margin-top:calc(45px + env(safe-area-inset-top)); } /* === Themenliste === */ .rm-card { box-sizing:border-box !important; display:block !important; width:100% !important; max-width:100% !important; margin:0 !important; padding:14px 14px 15px !important; border-bottom:1px solid rgba(128,128,128,.28) !important; } .rm-card:active { background:rgba(128,128,128,.14); } .rm-main { box-sizing:border-box !important; display:block !important; width:100% !important; max-width:100% !important; text-decoration:none !important; color:inherit !important; font-size:20px !important; font-weight:600 !important; line-height:1.32 !important; white-space:normal !important; word-break:normal !important; overflow-wrap:break-word !important; hyphens:none !important; } .rm-badge { display:inline; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; padding:2px 6px; margin-right:6px; border-radius:4px; background:rgba(200,60,60,.18); color:#e06a6a; white-space:nowrap; vertical-align:2px; } .rm-meta { margin-top:8px; font-size:13px; opacity:.7; overflow-wrap:break-word; } .rm-meta span + span::before { content:" · "; opacity:.55; } .rm-foot { margin-top:5px; display:flex; align-items:center; justify-content:space-between; gap:10px; } .rm-last { font-size:13px; opacity:.55; min-width:0; overflow-wrap:break-word; } .rm-jump { flex:none; font-size:12px; padding:5px 10px; border:1px solid rgba(128,128,128,.4); border-radius:99px; text-decoration:none !important; color:inherit !important; white-space:nowrap; } .rm-pages { padding:14px; display:flex; flex-wrap:wrap; gap:8px; } .rm-pages a { padding:6px 11px; border-radius:8px; border:1px solid rgba(128,128,128,.35); text-decoration:none !important; color:inherit !important; } /* === Thread === */ .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; } /* Die Masse muessen sich gegen die globale img-Regel durchsetzen. */ .rm-av { flex:none; width:28px !important; height:28px !important; 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); } // Originalinhalt ausblenden und die neue Ansicht als einziges sichtbares // body-Kind einhaengen. Alles, was spaeter noch an body soll, muss nach // diesem Aufruf kommen. function replaceBody(root) { Array.prototype.forEach.call(document.body.children, function (el) { if (el !== root) el.classList.add('rm-hidden'); }); document.body.appendChild(root); } // Ohne die Brotkrumen fuehrt aus der neuen Ansicht kein Weg zurueck: das // Original-DOM ist ausgeblendet, und phpBB verlinkt das Elternforum nirgends // sonst. Muss nach replaceBody() laufen, weil die Leiste an body haengt. function addCrumbs() { // Die Leiste steht zweimal im Markup, ueber und unter dem Inhalt. Die // erste genuegt; a.breadcrumbs faengt Seiten ohne den Absatz drumherum. var box = document.querySelector('p.breadcrumbs'); var items = box ? box.querySelectorAll('a') : document.querySelectorAll('a.breadcrumbs'); var bar = document.createElement('div'); bar.className = 'rm-crumbs'; var n = 0; Array.prototype.forEach.call(items, function (a) { var t = txt(a); if (!t) return; if (n) { var sep = document.createElement('span'); sep.className = 'rm-sep'; sep.textContent = '›'; bar.appendChild(sep); } var l = document.createElement('a'); l.href = a.href; l.textContent = t; bar.appendChild(l); n++; }); log.push('krumen=' + n); if (!n) return; document.body.appendChild(bar); document.body.classList.add('rm-crumbed'); } // ========================================================================== // Themenliste // ========================================================================== function breakable(s) { return s.replace(/([|\/\\,;:>\]\)])/g, '$1\u200B'); } function convert(tr) { var titleA = tr.querySelector('a.topictitle') || tr.querySelector('a[href*="viewtopic.php"]'); if (!titleA) return null; var titleCell = titleA.closest('td'); var nums = [], author = '', last = '', jump = null; Array.prototype.forEach.call(tr.children, function (td) { if (td === titleCell) return; var t = txt(td); if (/^\d+$/.test(t)) { nums.push(t); return; } var pa = td.querySelector('a[href*="profile.php"],a[href*="memberlist.php"]'); if (pa && t.length < 40 && !/\d{4}/.test(t)) { author = t; return; } if (t) { last = t; jump = td.querySelector('a[href*="#p"]') || td.querySelector('a[href*="viewtopic.php"]'); } }); var card = document.createElement('div'); card.className = 'rm-card'; var main = document.createElement('a'); main.className = 'rm-main'; main.href = titleA.href; var raw = txt(titleA); var badge = (raw.match(/^\[([^\]]{1,20})\]/) || [])[1]; if (badge) { var b = document.createElement('span'); b.className = 'rm-badge'; b.textContent = badge; main.appendChild(b); raw = raw.replace(/^\[[^\]]{1,20}\]\s*/, ''); } main.appendChild(document.createTextNode(breakable(raw))); card.appendChild(main); var meta = document.createElement('div'); meta.className = 'rm-meta'; function add(s) { var e = document.createElement('span'); e.textContent = s; meta.appendChild(e); } if (author) add(author); if (nums[0]) add(nums[0] + ' Antworten'); if (nums[1]) add(nums[1] + ' Views'); if (meta.children.length) card.appendChild(meta); var foot = document.createElement('div'); foot.className = 'rm-foot'; var l = document.createElement('div'); l.className = 'rm-last'; l.textContent = last; foot.appendChild(l); if (jump) { var j = document.createElement('a'); j.className = 'rm-jump'; j.href = jump.href; j.textContent = 'Neueste ↓'; foot.appendChild(j); } card.appendChild(foot); return card; } function buildList() { var links = document.querySelectorAll( 'a.topictitle,a[href*="viewtopic.php"]'); log.push('themenlinks=' + links.length); if (!links.length) return; var root = document.createElement('div'); root.className = 'rm-root'; var rows = [], tables = new Set(); links.forEach(function (a) { var tr = a.closest('tr'); if (!tr || tr.dataset.rm) return; var tbl = tr.closest('table'); if (!tbl) return; tr.dataset.rm = '1'; rows.push(tr); tables.add(tbl); }); var built = 0; rows.forEach(function (tr) { try { var c = convert(tr); if (c) { root.appendChild(c); built++; } } catch (e) { log.push('convert: ' + e.message); } }); log.push('karten=' + built); if (!built) return; var seen = {}, pages = document.createElement('div'); pages.className = 'rm-pages'; document.querySelectorAll('a[href*="start="]').forEach(function (a) { var t = txt(a); if (!t || t.length > 4 || seen[t]) return; seen[t] = 1; var n = document.createElement('a'); n.href = a.href; n.textContent = t; pages.appendChild(n); }); if (pages.children.length) root.appendChild(pages); replaceBody(root); addCrumbs(); if (DEBUG) { var c0 = root.querySelector('.rm-card'); log.push('fenster=' + window.innerWidth + ' body=' + document.body.scrollWidth + ' karte=' + (c0 ? Math.round(c0.getBoundingClientRect().width) : '?')); } } // ========================================================================== // Thread // ========================================================================== // 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 m2 = (el.textContent || '').match(/per_page\s*=\s*'?(\d+)/); if (m2) s.per = parseInt(m2[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 buildThread() { 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 rm-thread'; 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); replaceBody(root); addCrumbs(); } // ========================================================================== function run() { try { if (view === 'liste') buildList(); else if (view === 'thread') buildThread(); } catch (e) { log.push('build: ' + e.message); } if (DEBUG) { var d = document.createElement('div'); d.className = 'rm-dbg'; d.textContent = 'RIN Mobile v3.1\n' + log.join('\n'); document.body.insertBefore(d, document.body.firstChild); } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run); else run(); })();