mirror of
https://github.com/Andre-Bzk/rin-mobile-userscript.git
synced 2026-11-15 09:33:08 +01:00
Replacing the page hides the forum's own trail along with everything else, so a thread became a dead end: the only ways back to the parent forum were the browser's back button or editing the URL by hand. Deep in a long thread neither is workable. The bar has to be fixed rather than sticky. The view root carries overflow:hidden to contain phantom horizontal overflow, which would make it the scrollport a sticky bar sticks to — one that never scrolls. Its top clearance hangs off a body class so pages without a trail keep their full height instead of an empty strip. Board index still leads to a page the script does not rebuild; that would need a forum overview of its own. Debug flag off for daily use.
802 lines
30 KiB
JavaScript
802 lines
30 KiB
JavaScript
// ==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();
|
||
})();
|