// ==UserScript== // @name RIN Mobile // @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom // @version 3.8 // @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; // index.php wird ueber den Pfad geprueft, nicht ueber "here": der // Login-Redirect haengt "index.php" gelegentlich als Wert eines // Query-Parameters an ucp.php, ueber "here" faellt das faelschlich hier // hinein statt in den Login-Zweig. var view = /viewtopic\.php/.test(here) ? 'thread' : /viewforum\.php|search\.php/.test(here) || /\/index\.php$/.test(location.pathname) || location.pathname === '/forum/' ? 'liste' : /ucp\.php/.test(here) && /mode=login/.test(here) ? 'login' : ''; 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, alle 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 Seitenleiste. Die Klasse setzt die Leiste selbst, damit ohne sie kein leerer Streifen bleibt. */ .rm-root.rm-paged { padding-bottom:64px !important; } /* Brotkrumen, alle 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; } .rm-crumbs a.rm-auth { flex:none; margin-left:auto; padding:6px 12px !important; font-size:13px !important; border-radius:99px; background:rgba(128,128,128,.18); border:1px solid rgba(160,160,160,.4); } /* 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)); } /* === Themen- und Forenliste === */ .rm-sec { padding:18px 14px 6px; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; opacity:.55; } .rm-sec a { text-decoration:none !important; color:inherit !important; } .rm-desc { margin-top:6px; font-size:14px; opacity:.72; overflow-wrap:break-word; } .rm-subs { margin-top:9px; display:flex; flex-wrap:wrap; gap:6px; } .rm-subs a { font-size:12px; padding:4px 9px; border-radius:99px; border:1px solid rgba(128,128,128,.4); text-decoration:none !important; color:inherit !important; } /* Foren und Themen stehen auf einer Forenseite in derselben Tabelle untereinander. Im Kartenstrom unterscheiden sie sich sonst nur am Text, der Uebergang von den Unterforen zu den Themen ist nicht mehr zu sehen. */ .rm-group { display:flex; align-items:baseline; gap:10px; padding:11px 14px; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.07em; opacity:.75; background:rgba(128,128,128,.16); border-bottom:1px solid rgba(128,128,128,.28); cursor:pointer; -webkit-tap-highlight-color:transparent; -webkit-user-select:none; user-select:none; } .rm-group:active { background:rgba(128,128,128,.26); } .rm-group .rm-gname { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .rm-group .rm-count { flex:none; font-weight:600; letter-spacing:0; opacity:.7; } .rm-caret { flex:none; display:inline-block; font-size:10px; opacity:.7; transition:transform .15s; } .rm-group.rm-closed .rm-caret { transform:rotate(-90deg); } .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-card.rm-forum { border-left:3px solid rgba(120,170,255,.7); background:rgba(120,170,255,.07); } .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; } /* === 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; } /* === Login === */ .rm-login-title { padding:16px 14px 6px; font-size:21px; font-weight:650; line-height:1.3; } .rm-login-note { margin:8px 14px 4px; padding:10px 12px; font-size:13px; opacity:.75; border-radius:10px; background:rgba(128,128,128,.14); overflow-wrap:break-word; } .rm-login-note summary { cursor:pointer; font-weight:650; opacity:.9; } .rm-login-note[open] summary { margin-bottom:8px; } /* Die verschobene td.row1 wird per Klasse zum Block, wie form/table/tr ueber .rm-login form weiter unten. */ .rm-login-msg { display:block !important; box-sizing:border-box; padding:22px 14px calc(60px + env(safe-area-inset-bottom)) !important; text-align:center; font-size:16px; line-height:1.6; } .rm-login-msg a { display:inline-block; margin-top:8px; padding:12px 24px; font-size:16px; font-weight:650; border-radius:10px; color:#fff !important; text-decoration:none !important; background:rgba(120,170,255,.85); } /* Das Original-Formular ist tabellenbasiert und wird fuer den gestapelten Kartenstil komplett auf Block-Elemente umgelegt — derselbe Trick wie bei .rm-body table im Thread. Ohne Paginierung fehlt hier die .rm-nav, die sonst per .rm-root.rm-paged den unteren Rand freihaelt. Der grosse Bodensatz ist kein Stilmittel, sondern erzwingt genug Seitenhoehe zum Scrollen: ist die Seite so kurz wie der Viewport, bleibt Safaris eigene, nur teilweise eingefahrene Werkzeugleiste unten stehen und verdeckt den Button dauerhaft, weil nichts mehr zu scrollen ist, um sie wegzuschieben. */ .rm-login form { display:block; padding:4px 14px calc(140px + env(safe-area-inset-bottom)); } .rm-login form table, .rm-login form tbody, .rm-login form tr, .rm-login form td { display:block !important; width:100% !important; } .rm-login form th { display:none !important; } .rm-login form td { padding:0 !important; } .rm-login form b.gensmall { display:block; margin:16px 0 6px; font-size:13px; font-weight:650; opacity:.8; } .rm-login form input[type="text"], .rm-login form input[type="password"] { box-sizing:border-box; width:100%; font-size:16px; padding:11px 12px; border-radius:10px; color:#eee; background:rgba(120,120,120,.18); border:1px solid rgba(160,160,160,.35); } .rm-login form span.gensmall, .rm-login form a.gensmall { display:inline-block; margin-top:6px; font-size:12px; opacity:.6; text-decoration:none !important; color:inherit !important; } .rm-login form input[type="checkbox"] { width:18px; height:18px; vertical-align:middle; margin-right:8px; } .rm-login form input[type="submit"] { width:100%; font-size:16px; font-weight:650; padding:13px; margin-top:20px; border-radius:10px; color:#fff; border:0; background:rgba(120,170,255,.85); } /* === Seitennavigation, beide Ansichten === */ .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); } // Ob eine Sitzung besteht, verraet nur der Menuebalken der Originalseite: // ausgeloggt verlinkt er ucp.php?mode=login, eingeloggt stattdessen // mode=logout, ueblicherweise als "Logout [ Name ]". Das Namensmuster ist // ungetestet, da dem Projekt keine eingeloggte Fixture vorliegt — schlaegt // es fehl, bleibt die Pille ohne Namen stehen statt zu reissen. function loginState() { var out = document.querySelector('a[href*="mode=logout"]'); if (out) { var m = txt(out).match(/\[\s*(.+?)\s*\]/); return { in: true, href: out.href, name: m ? m[1] : '' }; } var into = document.querySelector('a[href*="mode=login"]'); return { in: false, href: into ? into.href : './ucp.php?mode=login' }; } // 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; var st = loginState(); var auth = document.createElement('a'); auth.className = 'rm-auth'; auth.href = st.href; auth.textContent = st.in ? (st.name || 'Logout') : 'Login'; bar.appendChild(auth); document.body.appendChild(bar); document.body.classList.add('rm-crumbed'); } // ========================================================================== // Seitennavigation, beide Ansichten // ========================================================================== // Foren- und Themenseiten paginieren gleich: derselbe start-Parameter, // dieselbe jumpto()-Funktion mit demselben per_page darin. Der Block gilt // deshalb fuer beide Ansichten. // --- 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' }); }); }; } // Die fixierte Leiste am unteren Rand. Der Aufrufer reicht den Zustand // herein, weil er allein entscheidet, ob die Leiste ueberhaupt lohnt: im // Thread gehoert sie zum Geruest, eine einseitige Liste braucht sie nicht. function pageNav(root, pg) { 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')); // Die Freihaltung haengt an der Leiste selbst: ohne sie stuende unten ein // leerer Streifen. root.classList.add('rm-paged'); root.appendChild(nav); } // ========================================================================== // Themenliste und Forenliste // ========================================================================== function breakable(s) { return s.replace(/([|\/\\,;:>\]\)])/g, '$1\u200B'); } // Karten teilen sich Titel, Zahlenzeile und Fusszeile; nur die Quellen // dafuer unterscheiden sich zwischen Thema und Forum. function card(titleA, badgeable, kind) { var el = document.createElement('div'); el.className = 'rm-card' + (kind === 'forum' ? ' rm-forum' : ''); var main = document.createElement('a'); main.className = 'rm-main'; main.href = titleA.href; var raw = txt(titleA); var badge = badgeable ? (raw.match(/^\[([^\]]{1,20})\]/) || [])[1] : null; 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))); el.appendChild(main); return el; } function metaRow(parts) { var meta = document.createElement('div'); meta.className = 'rm-meta'; parts.forEach(function (s) { if (!s) return; var e = document.createElement('span'); e.textContent = s; meta.appendChild(e); }); return meta.children.length ? meta : null; } function footRow(last, jump, label) { 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 = label; foot.appendChild(j); } return foot; } function convertTopic(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 el = card(titleA, true, 'thema'); var meta = metaRow([author, nums[0] ? nums[0] + ' Antworten' : '', nums[1] ? nums[1] + ' Views' : '']); if (meta) el.appendChild(meta); el.appendChild(footRow(last, jump, 'Neueste ↓')); return el; } // Forenzeilen tragen ihren Namen in a.forumlink, die Beschreibung und die // Unterforen stehen als weitere Absaetze in derselben Zelle. function convertForum(tr) { var titleA = forumTitle(tr); if (!titleA) return null; var titleCell = titleA.closest('td'); var nums = [], last = '', jump = null; Array.prototype.forEach.call(tr.children, function (td) { if (td === titleCell) return; var t = txt(td); if (/^\d[\d.,]*$/.test(t)) { nums.push(t); return; } if (t) { last = t; jump = td.querySelector('a[href*="viewtopic.php"]'); } }); var el = card(titleA, false, 'forum'); // Beschreibung, Moderatoren und Unterforen benutzen denselben Absatztyp. // Die beiden letzten sind nur an ihrer Einleitung zu erkennen. var desc = ''; if (titleCell) { titleCell.querySelectorAll('p.forumdesc, span.forumdesc') .forEach(function (p) { var t = txt(p); if (desc || !t || /^(Moderator|Subforum|Unterforum)/i.test(t)) return; desc = t; }); } // Ohne die Klasse steht die Beschreibung als blosser Text neben dem Namen. // Nur direkte Textknoten der Zelle: der Text der ganzen Zelle einzusammeln // holt bei einer verkannten Zeile den halben Seiteninhalt mit. if (!desc && titleCell) { var loose = ''; Array.prototype.forEach.call(titleCell.childNodes, function (n) { if (n.nodeType === 3) loose += ' ' + n.nodeValue; }); desc = loose.replace(/\s+/g, ' ').trim() .split(/Moderators?:|Subforums?:|Unterforen?:/)[0].trim(); if (desc.length > 300) desc = ''; } if (desc) { var d = document.createElement('div'); d.className = 'rm-desc'; d.textContent = desc; el.appendChild(d); } // Unterforen sind sonst nur ueber den Umweg des Elternforums erreichbar. var subs = document.createElement('div'); subs.className = 'rm-subs'; if (titleCell) { titleCell.querySelectorAll('a[href*="viewforum.php"]') .forEach(function (a) { if (a === titleA || !txt(a)) return; var s = document.createElement('a'); s.href = a.href; s.textContent = txt(a); subs.appendChild(s); }); } if (subs.children.length) el.appendChild(subs); var meta = metaRow([nums[0] ? nums[0] + ' Themen' : '', nums[1] ? nums[1] + ' Beiträge' : '']); if (meta) el.appendChild(meta); if (last || jump) el.appendChild(footRow(last, jump, 'Letzter ↓')); return el; } function forumTitle(tr) { var a = tr.querySelector('a.forumlink'); if (a) return a; // Ohne die Klasse bleibt der erste Link auf ein Forum, der Text traegt und // kein Unterforum ist. var found = null; tr.querySelectorAll('a[href*="viewforum.php"]').forEach(function (x) { if (found || !txt(x) || x.classList.contains('subforum') || x.classList.contains('breadcrumbs') || x.closest('p.breadcrumbs')) return; found = x; }); return found; } function section(a) { var el = document.createElement('div'); el.className = 'rm-sec'; var l = document.createElement('a'); l.href = a.href; l.textContent = txt(a); el.appendChild(l); return el; } // Ankuendigungen und Stickies stehen vor jedem Thema jeder Seite eines // Forums. Ihr Typ steht im Symbol der ersten Spalte, nicht am Titel: // global_read.png, announce_read.png, sticky_read.png gegen topic_read.png. function topicGroup(tr) { var im = tr.querySelector('img[src]'); var m = im && (im.getAttribute('src') || '') .match(/\b(global|announce|sticky)_(?:un)?read/); return m ? m[1] : 'thema'; } // phpBB stellt jeder dieser Gruppen eine Trennzeile voran. Sie traegt keinen // Link und faellt deshalb beim Einsammeln durch — ihr Text ist aber die // ehrlichste Beschriftung, die es fuer die Gruppe gibt, und deckt auch // Abschnitte ab, die dieses Script nicht kennt. function sepLabel(tr) { var p = tr.previousElementSibling, n = 0; while (p && n++ < 4) { if (p.dataset.rm) return ''; if (p.children.length === 1 && p.children[0].hasAttribute('colspan') && !p.querySelector('a, table')) { var t = txt(p); if (t && t.length < 40) return t; } p = p.previousElementSibling; } return ''; } var GNAME = { forum: 'Foren', thema: 'Themen', sticky: 'Stickies', global: 'Globale Ankündigungen', announce: 'Ankündigungen' }; // Genau die Gruppen, die zwischen dem Leser und den eigentlichen Themen // stehen, starten zugeklappt. var GSHUT = { global: 1, announce: 1, sticky: 1 }; // Der Zustand liegt in localStorage und nicht in GM_setValue: das kommt ohne // @grant aus und verhaelt sich in Tampermonkey und der iOS-App gleich. Der // Schluessel ist die Gruppenart, nicht ihr Name — einmal zugeklappt gilt // fuer jedes Forum. var GKEY = 'rm-groups'; function groupState() { try { return JSON.parse(localStorage.getItem(GKEY)) || {}; } catch (e) { return {}; } } function saveGroupState(group, open) { try { var s = groupState(); s[group] = open; localStorage.setItem(GKEY, JSON.stringify(s)); } catch (e) { log.push('speichern: ' + e.message); } } // Kopf ueber einem Block gleichartiger Karten, dazu der Kasten, in den sie // gehoeren: die Originalseite trennt Foren, Ankuendigungen, Stickies und // Themen ueber eigene Kopfzeilen, im Kartenstrom geht diese Grenze verloren. // Der Kopf ist zugleich der Schalter, der den Block zuklappt. function openGroup(root, group, label, n) { var saved = groupState()[group]; var open = saved === undefined ? !GSHUT[group] : !!saved; var head = document.createElement('div'); head.className = 'rm-group'; head.setAttribute('role', 'button'); var t = document.createElement('span'); t.className = 'rm-gname'; t.textContent = label; head.appendChild(t); var c = document.createElement('span'); c.className = 'rm-count'; c.textContent = n; head.appendChild(c); var caret = document.createElement('span'); caret.className = 'rm-caret'; caret.textContent = '▼'; head.appendChild(caret); var body = document.createElement('div'); body.className = 'rm-gbody'; function apply() { head.classList.toggle('rm-closed', !open); body.classList.toggle('rm-hidden', !open); head.setAttribute('aria-expanded', open ? 'true' : 'false'); } apply(); head.addEventListener('click', function () { open = !open; apply(); saveGroupState(group, open); }); root.appendChild(head); root.appendChild(body); return body; } // Welche Karte eine Zeile ergibt, entscheidet der erste verwertbare Link // darin. Beiwerk wie die Brotkrumen verlinkt ebenfalls auf viewforum.php, // steht aber in keiner Zeile, die eine Karte hergibt — deshalb muss eine // Forenzeile als solche erkennbar sein. function rowKind(a, tr) { if (!txt(a) || a.classList.contains('breadcrumbs') || a.closest('p.breadcrumbs')) return ''; // "Mark topics read" (eingeloggt oberhalb der Themenliste) und "Mark // forums read" verlinken mit ?mark=topics/forums auf die aktuelle Seite // selbst. Der Link liegt zwar in einer eigenen inneren