fix: resolve author and avatar detection in thread view

The forum nests each post body inside its own table, so ascending to the
nearest enclosing row stopped within that inner table rather than at the
actual post row. The author and profile columns were never in scope, and
every post silently degraded to the placeholder name and the generated
initial avatar instead of the real ones.

Author lookup additionally keyed on a profile link attribute this forum
does not emit at all, so even a correct row would have yielded nothing.
Both conditions failed quietly, which made the result look like a styling
problem rather than a broken DOM traversal.
This commit is contained in:
Andre
2026-08-02 13:44:54 +02:00
commit 46a1ed8d07
6 changed files with 851 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
dev/*.php
+282
View File
@@ -0,0 +1,282 @@
# iOS Safari Userscripts — cs.rin.ru
[English](#english) | [Deutsch](#deutsch)
---
## English
Two userscripts that rebuild the phpBB2 pages of `cs.rin.ru` into a mobile card
layout for Safari on iOS. Both replace the original table layout entirely: the
existing page content is hidden and a freshly built DOM is appended to `<body>`.
| Script | Matches | Purpose |
|---|---|---|
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Topic lists (`viewforum.php`, `search.php`) as cards |
| `rin-thread.user.js` | `*://cs.rin.ru/forum/viewtopic.php*` | Single thread as a post stream with a fixed bottom navigation bar |
Both scripts run at `document-start`, inject a mobile viewport meta tag, and
carry a `DEBUG` flag. While `DEBUG` is `true`, a green diagnostic overlay is
prepended to the page showing how many rows were found and converted. Set it to
`false` in both files before daily use.
### Installation on iOS
1. Install a userscript manager for Safari on iOS (for example *Userscripts*)
and enable it under *Settings → Safari → Extensions*.
2. Add `rin-mobile.user.js` and `rin-thread.user.js` to the manager's script
directory.
3. Grant the extension permission for `cs.rin.ru`.
---
### Development
The scripts are developed on Windows in Chrome or Edge with Tampermonkey. The
files in `dev/` are loaders: they contain nothing but a metadata block that
pulls in the real script from disk via `@require`. Editing
`rin-mobile.user.js` or `rin-thread.user.js` and reloading the page is enough —
no reinstall in Tampermonkey.
#### ⚠️ Path adjustment required
The loaders in `dev/` reference the scripts by **absolute path**:
```js
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.js
```
This path is hardcoded and only valid on the machine it was written for. After
cloning the repository or moving the project folder, adjust the `@require` line
in **both** loaders to your own location:
- `dev/rin-mobile.dev.user.js`
- `dev/rin-thread.dev.user.js`
Use forward slashes and the `file:///` prefix, including the drive letter —
`file:///D:/projects/ios_safari_userscripts/rin-mobile.user.js`. A `@require`
that cannot be resolved causes Tampermonkey to skip the script **entirely and
without an error message on the page**, which looks exactly like the script not
being installed at all.
#### Tampermonkey setup
Three settings are required; missing the first one is the most common cause of
"nothing happens":
1. `chrome://extensions` → Tampermonkey → enable **Allow user scripts**.
On older Chrome versions, enable **Developer mode** at the top right
instead. Without this, Manifest V3 blocks all injection.
2. Same page → enable **Allow access to file URLs**, so `@require file://`
can be resolved.
3. Tampermonkey dashboard → *Settings* → *Externals* → set the update interval
to **always**, otherwise the loader keeps serving a cached copy of the
script.
Then open the loaders once via `file:///.../dev/rin-mobile.dev.user.js` —
Tampermonkey intercepts `.user.js` URLs and offers to install them.
Do not run the loaders and the plain scripts side by side. If both are enabled
in Tampermonkey, the code executes twice on the same page, producing duplicate
cards and two debug overlays.
#### Test fixtures
`cs.rin.ru` requires a login, so save the pages you need once and iterate
against local copies:
1. Open the page while logged in, then in DevTools right-click `<html>` →
*Copy outerHTML*.
2. Save it as `dev/viewtopic.php.html` or `dev/viewforum.php.html`.
3. Serve the folder: `python -m http.server 8777 --directory .`
The file name matters twice over. The `.html` extension makes the server send
the file as HTML instead of offering it as a download, and the retained
`viewtopic.php` / `viewforum.php` part is what the loaders' `@include` rules
and the page-type check in `rin-mobile.user.js` match against.
Forum images and stylesheets are missing from such a snapshot. That is fine for
layout work, because the scripts rebuild the DOM anyway — only avatars stay
empty, since their `src` is carried over unchanged.
Note that a snapshot saved while logged out contains `span.link_removed`
placeholders instead of links, and may use different markup for author names
than the logged-in view.
#### Device emulation
DevTools → *Device Toolbar* (`Ctrl+Shift+M`) → *Edit…* → add a custom device
with **430 × 932** and DPR **3** for an iPhone 15 Pro Max, device type *Mobile*,
and an iOS Safari user agent string.
#### What desktop emulation cannot show
Chrome uses Blink, iOS Safari uses WebKit. These points have to be verified on
the device:
- `env(safe-area-inset-bottom)` in `.rm-nav` — always `0` in the emulator, so
the clearance for the home indicator is untested.
- A bottom `position: fixed` bar together with Safari's collapsing toolbar.
- `width: 100vw` — on the desktop the scrollbar counts towards it and creates
phantom overflow, so the debug overlay reports different numbers than iOS.
- `-webkit-text-size-adjust` and Safari's automatic font scaling.
- `-webkit-backdrop-filter` renders in Chrome, but looks different.
For higher fidelity without an iPhone, Playwright ships a real WebKit build for
Windows (`npx playwright install webkit`).
#### Troubleshooting
Install `dev/tm-test.user.js` — it injects a red bar and nothing else. If the
bar does not appear, the problem is Tampermonkey (see setup above), not the
script code. If it does appear, check the loader's *Externals* tab in the
Tampermonkey dashboard: an empty or failed entry means the `@require` path is
wrong.
If Tampermonkey keeps serving a stale copy despite the update interval, replace
the `file://` require with a local HTTP server, which does not cache:
```js
// @require http://localhost:8777/rin-mobile.user.js
```
### Repository layout
```
rin-mobile.user.js topic list → cards
rin-thread.user.js thread → post stream
dev/
rin-mobile.dev.user.js loader, @require with absolute path
rin-thread.dev.user.js loader, @require with absolute path
```
---
## Deutsch
Zwei Userscripts, die die phpBB2-Seiten von `cs.rin.ru` für Safari auf iOS in
eine mobile Kartenansicht umbauen. Beide ersetzen das ursprüngliche
Tabellenlayout vollständig: der vorhandene Seiteninhalt wird ausgeblendet und
ein neu gebautes DOM an `<body>` gehängt.
| Script | Greift auf | Zweck |
|---|---|---|
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Themenlisten (`viewforum.php`, `search.php`) als Karten |
| `rin-thread.user.js` | `*://cs.rin.ru/forum/viewtopic.php*` | Einzelner Thread als Beitragsstrom mit fixierter Navigationsleiste |
Beide laufen bei `document-start`, setzen ein mobiles Viewport-Meta-Tag und
haben ein `DEBUG`-Flag. Solange `DEBUG` auf `true` steht, wird ein grünes
Diagnose-Overlay über die Seite gelegt, das zeigt, wie viele Zeilen gefunden und
umgewandelt wurden. Für den täglichen Gebrauch in beiden Dateien auf `false`
setzen.
### Installation unter iOS
1. Einen Userscript-Manager für Safari unter iOS installieren (zum Beispiel
*Userscripts*) und unter *Einstellungen → Safari → Erweiterungen* aktivieren.
2. `rin-mobile.user.js` und `rin-thread.user.js` in das Script-Verzeichnis des
Managers legen.
3. Der Erweiterung die Berechtigung für `cs.rin.ru` erteilen.
---
### Entwicklung
Entwickelt wird unter Windows in Chrome oder Edge mit Tampermonkey. Die Dateien
in `dev/` sind Loader: sie enthalten nur einen Metadatenblock, der das
eigentliche Script per `@require` von der Platte einbindet. Es genügt,
`rin-mobile.user.js` oder `rin-thread.user.js` zu bearbeiten und die Seite neu
zu laden — keine Neuinstallation in Tampermonkey nötig.
#### ⚠️ Pfadanpassung erforderlich
Die Loader in `dev/` verweisen mit einem **absoluten Pfad** auf die Scripts:
```js
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.js
```
Dieser Pfad ist fest eingetragen und gilt nur auf dem Rechner, für den er
geschrieben wurde. Nach dem Klonen des Repositories oder dem Verschieben des
Projektordners muss die `@require`-Zeile in **beiden** Loadern auf den eigenen
Ablageort angepasst werden:
- `dev/rin-mobile.dev.user.js`
- `dev/rin-thread.dev.user.js`
Dabei Schrägstriche und das Präfix `file:///` inklusive Laufwerksbuchstaben
verwenden — `file:///D:/projekte/ios_safari_userscripts/rin-mobile.user.js`.
Ein `@require`, das nicht aufgelöst werden kann, führt dazu, dass Tampermonkey
das Script **komplett und ohne Fehlermeldung auf der Seite** überspringt. Das
sieht exakt so aus, als wäre gar kein Script installiert.
#### Tampermonkey einrichten
Drei Einstellungen sind nötig; die erste ist die mit Abstand häufigste Ursache
für „es passiert überhaupt nichts":
1. `chrome://extensions` → Tampermonkey → **Nutzerskripte zulassen**
aktivieren. Bei älteren Chrome-Versionen stattdessen den
**Entwicklermodus** oben rechts einschalten. Ohne das blockiert Manifest V3
jede Injektion.
2. Auf derselben Seite → **Zugriff auf Datei-URLs zulassen** aktivieren, damit
`@require file://` aufgelöst werden kann.
3. Tampermonkey-Dashboard → *Einstellungen* → *Externals* → Update-Intervall auf
**immer** stellen, sonst liefert der Loader weiterhin eine zwischen­
gespeicherte Fassung des Scripts aus.
Danach die Loader einmal über `file:///.../dev/rin-mobile.dev.user.js` öffnen —
Tampermonkey fängt `.user.js`-URLs ab und bietet die Installation an.
Die Loader nicht parallel zu den unveränderten Scripts betreiben. Sind beide in
Tampermonkey aktiv, läuft der Code doppelt auf derselben Seite, mit doppelten
Karten und zwei Debug-Overlays als Folge.
#### Geräte-Emulation
DevTools → *Device Toolbar* (`Strg+Umschalt+M`) → *Edit…* → eigenes Gerät mit
**430 × 932** und DPR **3** für ein iPhone 15 Pro Max anlegen, Gerätetyp
*Mobile*, dazu einen iOS-Safari-User-Agent eintragen.
#### Was die Desktop-Emulation nicht zeigt
Chrome verwendet Blink, iOS Safari verwendet WebKit. Diese Punkte müssen am
Gerät geprüft werden:
- `env(safe-area-inset-bottom)` in `.rm-nav` — im Emulator immer `0`, die
Freihaltung für den Home-Indicator bleibt also ungetestet.
- Eine unten fixierte Leiste zusammen mit Safaris ein- und ausfahrender
Toolbar.
- `width: 100vw` — auf dem Desktop zählt die Scrollbar mit und erzeugt
Phantom-Overflow, das Debug-Overlay meldet dort andere Zahlen als unter iOS.
- `-webkit-text-size-adjust` und Safaris automatische Schriftvergrößerung.
- `-webkit-backdrop-filter` rendert in Chrome, sieht aber anders aus.
Für höhere Treue ohne iPhone bringt Playwright einen echten WebKit-Build für
Windows mit (`npx playwright install webkit`).
#### Fehlersuche
`dev/tm-test.user.js` installieren — das Script blendet nur einen roten Balken
ein, sonst nichts. Erscheint der Balken nicht, liegt das Problem bei
Tampermonkey (siehe Einrichtung oben) und nicht am Script-Code. Erscheint er,
im Tampermonkey-Dashboard den Tab *Externals* des Loaders prüfen: ein leerer
oder fehlgeschlagener Eintrag bedeutet, dass der `@require`-Pfad falsch ist.
Liefert Tampermonkey trotz Update-Intervall weiterhin einen veralteten Stand
aus, das `file://`-Require durch einen lokalen HTTP-Server ersetzen, der nicht
zwischenspeichert:
```js
// @require http://localhost:8777/rin-mobile.user.js
```
### Aufbau des Repositories
```
rin-mobile.user.js Themenliste → Karten
rin-thread.user.js Thread → Beitragsstrom
dev/
rin-mobile.dev.user.js Loader, @require mit absolutem Pfad
rin-thread.dev.user.js Loader, @require mit absolutem Pfad
```
+15
View File
@@ -0,0 +1,15 @@
// ==UserScript==
// @name [DEV] RIN Mobile
// @namespace rin-dev
// @description Dev-Loader: laedt rin-mobile.user.js live von der Platte
// @version 1.0
// @match *://cs.rin.ru/forum/*
// @include http://localhost:*/*viewforum*
// @include http://127.0.0.1:*/*viewforum*
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.js
// @run-at document-start
// @grant none
// ==/UserScript==
// Der eigentliche Code steckt in der @require-Datei.
// Aenderungen dort greifen nach einem Reload der Seite.
+15
View File
@@ -0,0 +1,15 @@
// ==UserScript==
// @name [DEV] RIN Mobile Thread
// @namespace rin-dev
// @description Dev-Loader: laedt rin-thread.user.js live von der Platte
// @version 1.0
// @match *://cs.rin.ru/forum/viewtopic.php*
// @include http://localhost:*/*viewtopic*
// @include http://127.0.0.1:*/*viewtopic*
// @require file:///C:/Projekte/ios_safari_userscripts/rin-thread.user.js
// @run-at document-start
// @grant none
// ==/UserScript==
// Der eigentliche Code steckt in der @require-Datei.
// Aenderungen dort greifen nach einem Reload der Seite.
+236
View File
@@ -0,0 +1,236 @@
// ==UserScript==
// @name RIN Mobile
// @description Mobile Karten-Ansicht fuer cs.rin.ru
// @version 1.4
// @match *://cs.rin.ru/forum/*
// @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; }
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-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; }
.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; }
.rm-hidden { display:none !important; }
`;
h().appendChild(st);
function txt(el) { return (el.textContent || '').replace(/\s+/g, ' ').trim(); }
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 build() {
var onList = /viewforum\.php|search\.php/
.test(location.pathname + location.search) ||
location.pathname === '/forum/';
log.push('listenseite=' + onList);
if (!onList) return;
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);
// Originalinhalt ausblenden, Liste als direktes body-Kind einhaengen
Array.prototype.forEach.call(document.body.children, function (el) {
if (el !== root) el.classList.add('rm-hidden');
});
document.body.appendChild(root);
if (DEBUG) {
var c0 = root.querySelector('.rm-card');
log.push('fenster=' + window.innerWidth +
' body=' + document.body.scrollWidth +
' karte=' + (c0 ? Math.round(c0.getBoundingClientRect().width) : '?'));
}
}
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 Mobile v1.4\n' + log.join('\n');
document.body.insertBefore(d, document.body.firstChild);
}
}
if (document.readyState === 'loading')
document.addEventListener('DOMContentLoaded', run);
else
run();
})();
+302
View File
@@ -0,0 +1,302 @@
// ==UserScript==
// @name RIN Mobile Thread
// @description Mobile Beitragsansicht fuer cs.rin.ru
// @version 2.2
// @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-pos { opacity:.6; font-size:13px; }
.rm-nav .rm-off { opacity:.28; border-color:rgba(160,160,160,.2); }
`;
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;
}
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 nav = document.createElement('div');
nav.className = 'rm-nav';
var prevA = null, nextA = null;
document.querySelectorAll('a').forEach(function (a) {
var t = txt(a);
if (!prevA && /^(Previous|Vorherige)$/i.test(t)) prevA = a;
if (!nextA && /^(Next|Weiter|Nächste)$/i.test(t)) nextA = a;
});
var pos = txt(document.body).match(/Page\s+(\d+)\s+of\s+(\d+)/i);
function navBtn(a, label) {
var e = document.createElement('a');
e.textContent = label;
if (a) e.href = a.href; else e.className = 'rm-off';
return e;
}
nav.appendChild(navBtn(prevA, '‹ Zurück'));
var ps = document.createElement('span');
ps.className = 'rm-pos';
ps.textContent = pos ? pos[1] + ' / ' + pos[2] : '';
nav.appendChild(ps);
nav.appendChild(navBtn(nextA, 'Weiter ›'));
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.2\n' + log.join('\n');
document.body.insertBefore(d, document.body.firstChild);
}
}
if (document.readyState === 'loading')
document.addEventListener('DOMContentLoaded', run);
else run();
})();