refactor: merge the two userscripts into one

The list script matched the whole forum, thread pages included, and injected its
stylesheet there before bailing out of its own build. Both stylesheets were
therefore live on every thread, and rules sharing a class name resolved by
userscript load order rather than by intent — a layout that could differ between
two machines with the same files installed. One of those clashes was already
costing the clearance behind the fixed bar.

A single script picks its view from the page type instead, which turns the
shared class namespace from a hazard into a plain fact of the file, resolved
with a modifier on the root element. Pages outside either view now stay
untouched, where before they still received a full stylesheet.

Two things the merge brought to light: the global image rule was overriding the
avatar dimensions, which only went unnoticed because square avatars happen to
scale to the same result, and a zero-width space had been sitting in the source
as a literal character rather than an escape.

Installations carrying the old thread script have to remove it; the remaining
one keeps its name and updates in place.

Claude-Session: https://claude.ai/code/session_0158owAAsXAfFiT3hGkQt1Xk
This commit is contained in:
Andre
2026-08-02 14:24:29 +02:00
parent bd4fc85d38
commit eff957ed07
6 changed files with 634 additions and 665 deletions
+45 -35
View File
@@ -1,54 +1,59 @@
# CLAUDE.md
Zwei eigenstaendige Userscripts, die die phpBB-Seiten von `cs.rin.ru` fuer
Safari auf iOS in eine mobile Ansicht umbauen. Kein Build, keine
Abhaengigkeiten, kein `package.json` — die `.user.js`-Dateien sind das Produkt.
Ein Userscript, das die phpBB-Seiten von `cs.rin.ru` fuer Safari auf iOS in eine
mobile Ansicht umbaut. Kein Build, keine Abhaengigkeiten, kein `package.json` —
die `.user.js`-Datei ist das Produkt.
| Datei | Greift auf | Zweck |
|---|---|---|
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Themenlisten als Karten |
| `rin-thread.user.js` | `*://cs.rin.ru/forum/viewtopic.php*` | Thread als Beitragsstrom + Navigationsleiste |
| `dev/*.dev.user.js` | zusaetzlich `localhost:*` | Loader, binden per `@require file:///` die echten Scripts ein |
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Beide Ansichten, Weiche nach Seitentyp |
| `dev/rin-mobile.dev.user.js` | zusaetzlich `localhost:*` | Loader, bindet per `@require file:///` das echte Script ein |
| `dev/viewtopic.php` | — | Fixture: gespeicherter Thread, 1114 Seiten |
Das Fixture ist ueber `.gitignore` (`dev/*.php`) ausgeschlossen. Nach einem
frischen Klon ist es nicht da und muss nach der Anleitung in der README neu
gespeichert werden — die Seite verlangt einen Login.
## Gemeinsames Muster beider Scripts
## Aufbau
Beide sind nach demselben Schema gebaut, Aenderungen am einen sind oft auch am
anderen faellig:
Ein IIFE, streng der Reihe nach:
1. IIFE mit `'use strict'`, `DEBUG`-Flag und `log`-Array ganz oben
2. Viewport-Meta setzen (idempotent ueber `[data-rm]`)
3. Kompletter Style-Block als Template-Literal, **bedingungslos** injiziert
4. `build()` liest das Original-DOM, baut ein neues `div.rm-root`
5. Alle bisherigen `body`-Kinder bekommen `.rm-hidden`, dann wird `root`
angehaengt
6. `run()` haengt bei `DEBUG` das gruene Overlay oben an
1. `DEBUG`-Flag und `log`-Array
2. Weiche: `view` ist `'thread'`, `'liste'` oder `''` — abgeleitet aus
`location.pathname + location.search`
3. Nur bei gesetztem `view`: Viewport-Meta (idempotent ueber `[data-rm]`) und
der komplette Style-Block als Template-Literal
4. `replaceBody(root)` blendet alle bisherigen `body`-Kinder aus und haengt die
neue Ansicht an
5. `buildList()` beziehungsweise `buildThread()` lesen das Original-DOM und
bauen ein `div.rm-root`
6. `run()` waehlt die Ansicht und haengt bei `DEBUG` das gruene Overlay an
7. Start ueber `DOMContentLoaded`, falls `readyState === 'loading'`
Auf Seiten ausserhalb der Weiche (`posting.php`, Profile, …) passiert nichts,
insbesondere wird kein Stylesheet injiziert.
## Fallen, die nicht aus dem Code hervorgehen
**Beide Stylesheets sind auf Threadseiten gleichzeitig aktiv.**
`rin-mobile` matcht `*://cs.rin.ru/forum/*`, also auch `viewtopic.php`. Sein
`build()` steigt dort zwar aus, der Style-Block wird aber vorher injiziert.
Gleich benannte Klassen kollidieren mit identischer Spezifitaet, entschieden
wird nach Ladereihenfolge der Userscripts — und die ist nicht garantiert.
**Beide Ansichten teilen sich einen Klassennamensraum.** Das war der Grund fuer
die Zusammenlegung: als getrennte Scripts matchte `rin-mobile` auch
`viewtopic.php` und injizierte dort sein Stylesheet, obwohl es nichts baute.
Gleich benannte Regeln kollidierten mit identischer Spezifitaet, und die
Ladereihenfolge der Userscripts entschied ueber das Layout.
- Nur bewusst geteilt: `.rm-root`, `.rm-hidden`, `.rm-dbg`
- **Offene Kollision:** `.rm-root` hat in `rin-mobile` `padding:0 !important`,
in `rin-thread` `padding:0 0 64px 0 !important`. Laedt `rin-mobile` spaeter,
faellt die Freihaltung hinter der fixierten Leiste weg. Noch nicht behoben.
- Neue Klassen im Thread-Script gegen `rin-mobile.user.js` gegenpruefen.
`.rm-goto` und `.rm-grid` heissen genau deshalb so — `.rm-jump` und
`.rm-pages` waren dort schon belegt.
- Geteilt: `.rm-root`, `.rm-hidden`, `.rm-dbg`, die globale `img`-Regel
- Unterschiede zwischen den Ansichten ueber einen Modifier am Root loesen,
nicht ueber zwei Regeln mit demselben Selektor. `.rm-root.rm-thread` haelt
so die 64px hinter der fixierten Leiste frei.
- Namen sind ansichtsspezifisch zu halten: `.rm-jump`/`.rm-pages` gehoeren der
Liste, `.rm-goto`/`.rm-grid` dem Thread-Sheet.
- `.rm-av` braucht `!important` auf Breite und Hoehe, sonst gewinnt die globale
`img { height:auto !important }`-Regel gegen die Avatarmasse.
**Was nach `body` soll, muss nach der Ausblend-Schleife angehaengt werden.**
Die Schleife markiert *alle* vorhandenen `body`-Kinder mit `.rm-hidden`. Das
Sheet im Thread-Script wird deshalb erst beim ersten Antippen gebaut — das
loest das Problem nebenbei mit.
**Was nach `body` soll, muss nach `replaceBody()` angehaengt werden.**
Die Funktion markiert *alle* vorhandenen `body`-Kinder mit `.rm-hidden`. Das
Sheet wird deshalb erst beim ersten Antippen gebaut — das loest das Problem
nebenbei mit.
**`Page X of Y` steht als `Page <strong>1</strong> of <strong>1114</strong>`
im Markup.** Ein Grep im Roh-HTML nach `Page 1 of` findet nichts, die Regex auf
@@ -65,9 +70,9 @@ Original-Zurueck/Weiter-Links, das Sheet verschwindet.
einer verschachtelten Tabelle, `closest('tr')` trifft die innere Zeile ohne
Autor- und Profilspalte.
**`DEBUG` steht in beiden Dateien auf `true`** und muss vor dem Ausliefern in
beiden auf `false`. Die Versionsnummer steht ebenfalls doppelt: im
`@version`-Header und im Text des Debug-Overlays.
**`DEBUG` steht auf `true`** und muss vor dem Ausliefern auf `false`. Die
Versionsnummer steht doppelt: im `@version`-Header und im Text des
Debug-Overlays.
## Testen
@@ -92,6 +97,11 @@ Sinnvolle Faelle: erste Seite, zweite Seite, Mitte, letzte Seite, dazu die
Degradation ohne `per_page` und ein Thread ohne Paginierung. Fixture-Eckdaten:
`f=10&t=68689`, `per_page = 15`, 1114 Seiten, letzte Seite `start=16695`.
Fuer die Weiche und die Listenansicht genuegt eine handgeschriebene Tabelle mit
`a.topictitle`-Zeilen. Zu pruefen ist dabei auch eine Seite ausserhalb der
Weiche wie `posting.php`: dort darf weder eine Ansicht gebaut noch ein
`<style>` injiziert werden.
Am Geraet zu pruefen bleibt, was Blink nicht zeigt: `env(safe-area-inset-*)`,
fixierte Leisten gegen Safaris ein- und ausfahrende Toolbar,
`-webkit-backdrop-filter`, Schriftskalierung. Details und die
+57 -61
View File
@@ -6,14 +6,18 @@
## 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
A single userscript that rebuilds the phpBB2 pages of `cs.rin.ru` into a mobile
layout for Safari on iOS. It replaces 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 |
`rin-mobile.user.js` matches `*://cs.rin.ru/forum/*` and picks its view from the
page type:
| Page | View |
|---|---|
| `viewforum.php`, `search.php`, forum root | Topic list as cards |
| `viewtopic.php` | Thread as a post stream with a fixed bottom navigation bar |
| anything else | Untouched — no styles are injected |
The thread navigation bar holds first / previous / next / last and a page
indicator. Tapping the indicator opens a sheet with a number field and a grid of
@@ -23,32 +27,31 @@ them. `per_page` is read from the `jumpto()` function the forum embeds in the
page; without it, only first page and the original previous/next links remain
available.
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.
The script runs at `document-start`, injects a mobile viewport meta tag, and
carries a `DEBUG` flag. While `DEBUG` is `true`, a green diagnostic overlay is
prepended to the page showing which view was chosen and how many rows were
converted. Set it to `false` 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.
2. Add `rin-mobile.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.
The script is developed on Windows in Chrome or Edge with Tampermonkey.
`dev/rin-mobile.dev.user.js` is a loader: it contains nothing but a metadata
block that pulls in the real script from disk via `@require`. Editing
`rin-mobile.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**:
The loader in `dev/` references the script by **absolute path**:
```js
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.js
@@ -56,10 +59,7 @@ The loaders in `dev/` reference the scripts by **absolute path**:
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`
in `dev/rin-mobile.dev.user.js` to your own location.
Use forward slashes and the `file:///` prefix, including the drive letter —
`file:///D:/projects/ios_safari_userscripts/rin-mobile.user.js`. A `@require`
@@ -81,11 +81,11 @@ Three settings are required; missing the first one is the most common cause of
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.
Then open the loader once via `file:///.../dev/rin-mobile.dev.user.js` —
Tampermonkey intercepts `.user.js` URLs and offers to install it.
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
Do not run the loader and the plain script 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
@@ -100,11 +100,11 @@ against local copies:
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
`viewtopic.php` / `viewforum.php` part is what the loader's `@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
layout work, because the script rebuilds 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`
@@ -151,26 +151,28 @@ the `file://` require with a local HTTP server, which does not cache:
### Repository layout
```
rin-mobile.user.js topic list → cards
rin-thread.user.js thread → post stream
rin-mobile.user.js topic list → cards, 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.
Ein Userscript, das die phpBB2-Seiten von `cs.rin.ru` für Safari auf iOS in eine
mobile Ansicht umbaut. Es ersetzt 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 |
`rin-mobile.user.js` greift auf `*://cs.rin.ru/forum/*` und wählt die Ansicht
anhand des Seitentyps:
| Seite | Ansicht |
|---|---|
| `viewforum.php`, `search.php`, Forumwurzel | Themenliste als Karten |
| `viewtopic.php` | Thread als Beitragsstrom mit fixierter Navigationsleiste |
| alles andere | Unverändert — es werden keine Stile injiziert |
Die Navigationsleiste im Thread enthält erste / vorherige / nächste / letzte
Seite und eine Positionsanzeige. Ein Tipp darauf öffnet ein Sheet mit
@@ -181,33 +183,32 @@ Seitenlinks übernommen, weil phpBB davon immer nur eine Handvoll anzeigt.
einbettet; fehlt sie, bleiben nur die erste Seite und die ursprünglichen
Zurück-/Weiter-Links nutzbar.
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`
Das Script läuft bei `document-start`, setzt ein mobiles Viewport-Meta-Tag und
hat ein `DEBUG`-Flag. Solange `DEBUG` auf `true` steht, wird ein grünes
Diagnose-Overlay über die Seite gelegt, das zeigt, welche Ansicht gewählt wurde
und wie viele Zeilen umgewandelt wurden. Für den täglichen Gebrauch 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.
2. `rin-mobile.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.
Entwickelt wird unter Windows in Chrome oder Edge mit Tampermonkey.
`dev/rin-mobile.dev.user.js` ist ein Loader: er enthält nur einen
Metadatenblock, der das eigentliche Script per `@require` von der Platte
einbindet. Es genügt, `rin-mobile.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:
Der Loader in `dev/` verweist mit einem **absoluten Pfad** auf das Script:
```js
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.js
@@ -215,11 +216,8 @@ Die Loader in `dev/` verweisen mit einem **absoluten Pfad** auf die Scripts:
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`
Projektordners muss die `@require`-Zeile in `dev/rin-mobile.dev.user.js` auf den
eigenen Ablageort angepasst werden.
Dabei Schrägstriche und das Präfix `file:///` inklusive Laufwerksbuchstaben
verwenden — `file:///D:/projekte/ios_safari_userscripts/rin-mobile.user.js`.
@@ -242,10 +240,10 @@ für „es passiert überhaupt nichts":
**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 —
Danach den 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
Den Loader nicht parallel zum unveränderten Script betreiben. Sind beide in
Tampermonkey aktiv, läuft der Code doppelt auf derselben Seite, mit doppelten
Karten und zwei Debug-Overlays als Folge.
@@ -291,9 +289,7 @@ zwischenspeichert:
### Aufbau des Repositories
```
rin-mobile.user.js Themenliste → Karten
rin-thread.user.js Thread → Beitragsstrom
rin-mobile.user.js Themenliste → Karten, Thread → Beitragsstrom
dev/
rin-mobile.dev.user.js Loader, @require mit absolutem Pfad
rin-thread.dev.user.js Loader, @require mit absolutem Pfad
```
+3 -1
View File
@@ -2,10 +2,12 @@
// @name [DEV] RIN Mobile
// @namespace rin-dev
// @description Dev-Loader: laedt rin-mobile.user.js live von der Platte
// @version 1.0
// @version 2.0
// @match *://cs.rin.ru/forum/*
// @include http://localhost:*/*viewforum*
// @include http://127.0.0.1:*/*viewforum*
// @include http://localhost:*/*viewtopic*
// @include http://127.0.0.1:*/*viewtopic*
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.js
// @run-at document-start
// @grant none
-15
View File
@@ -1,15 +0,0 @@
// ==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.
+525 -23
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name RIN Mobile
// @description Mobile Karten-Ansicht fuer cs.rin.ru
// @version 1.4
// @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom
// @version 3.0
// @match *://cs.rin.ru/forum/*
// @run-at document-start
// ==/UserScript==
@@ -14,7 +14,22 @@
function h() { return document.head || document.documentElement; }
if (!document.querySelector('meta[name="viewport"][data-rm]')) {
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';
@@ -22,8 +37,11 @@
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;
@@ -33,12 +51,17 @@
.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;
.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; }
/* === Themenliste === */
.rm-card { box-sizing:border-box !important;
display:block !important;
width:100% !important; max-width:100% !important;
@@ -76,11 +99,132 @@
.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; }
/* === 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);
}
function txt(el) { return (el.textContent || '').replace(/\s+/g, ' ').trim(); }
// 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);
}
// ==========================================================================
// Themenliste
// ==========================================================================
function breakable(s) {
return s.replace(/([|\/\\,;:>\]\)])/g, '$1\u200B');
@@ -156,13 +300,7 @@
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;
function buildList() {
var links = document.querySelectorAll(
'a.topictitle,a[href*="viewtopic.php"]');
log.push('themenlinks=' + links.length);
@@ -205,11 +343,7 @@
});
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);
replaceBody(root);
if (DEBUG) {
var c0 = root.querySelector('.rm-card');
@@ -219,18 +353,386 @@
}
}
// ==========================================================================
// 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);
}
// ==========================================================================
function run() {
try { build(); } catch (e) { log.push('build: ' + e.message); }
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 v1.4\n' + log.join('\n');
d.textContent = 'RIN Mobile v3.0\n' + log.join('\n');
document.body.insertBefore(d, document.body.firstChild);
}
}
if (document.readyState === 'loading')
document.addEventListener('DOMContentLoaded', run);
else
run();
else run();
})();
-526
View File
@@ -1,526 +0,0 @@
// ==UserScript==
// @name RIN Mobile Thread
// @description Mobile Beitragsansicht fuer cs.rin.ru
// @version 2.3
// @match *://cs.rin.ru/forum/viewtopic.php*
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
var DEBUG = true;
var log = [];
function h() { return document.head || document.documentElement; }
if (!document.querySelector('meta[name="viewport"][data-rm]')) {
var m = document.createElement('meta');
m.name = 'viewport';
m.content = 'width=device-width, initial-scale=1, viewport-fit=cover';
m.setAttribute('data-rm', '1');
h().appendChild(m);
}
var st = document.createElement('style');
st.textContent = `
html, body { max-width:100% !important; overflow-x:hidden !important;
margin:0 !important; padding:0 !important; }
body { font-size:16px !important;
-webkit-text-size-adjust:100% !important; }
.rm-dbg { background:#222; color:#0f0; font:12px/1.45 monospace;
padding:8px 10px; white-space:pre-wrap; }
.rm-hidden { display:none !important; }
.rm-root { box-sizing:border-box !important; display:block !important;
width:100vw !important; max-width:100vw !important;
margin:0 !important; padding:0 0 64px 0 !important;
overflow:hidden !important; }
.rm-topic { box-sizing:border-box; padding:16px 14px 12px;
font-size:21px; font-weight:650; line-height:1.3;
overflow-wrap:break-word; }
.rm-post { box-sizing:border-box !important;
width:100% !important; max-width:100% !important;
padding:14px !important;
border-top:1px solid rgba(128,128,128,.25) !important; }
.rm-head { display:flex; align-items:center; gap:9px;
margin-bottom:10px; min-width:0; }
.rm-av { flex:none; width:28px; height:28px; border-radius:50%;
object-fit:cover; background:rgba(128,128,128,.25); }
.rm-who { min-width:0; flex:1; }
.rm-user { font-size:14px; font-weight:650; line-height:1.2;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rm-sub { font-size:11px; opacity:.55; line-height:1.3;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rm-body { font-size:17px !important; line-height:1.55 !important;
overflow-wrap:break-word !important;
word-break:normal !important; }
.rm-body * { max-width:100% !important; }
.rm-body img { height:auto !important; border-radius:6px; }
.rm-body a { overflow-wrap:anywhere !important; }
.rm-body table { display:block !important; overflow-x:auto !important;
width:auto !important; }
.rm-body pre, .rm-body code {
display:block; overflow-x:auto; white-space:pre;
font-size:13px; padding:10px; border-radius:8px;
background:rgba(128,128,128,.14); }
.rm-body blockquote, .rm-body .quote, .rm-body table.quote {
display:block !important; margin:12px 0 !important;
padding:8px 0 8px 12px !important;
border-left:3px solid rgba(140,140,255,.5) !important;
font-size:15px !important; opacity:.8 !important;
background:none !important; }
.rm-body ul, .rm-body ol { padding-left:22px !important; }
.rm-body hr { border:0; border-top:1px solid rgba(128,128,128,.3); }
.rm-sig { margin-top:12px; font-size:13px; opacity:.5; }
.rm-sig summary { cursor:pointer; opacity:.8; }
.rm-nav { position:fixed; left:0; right:0; bottom:0; z-index:9999;
display:flex; align-items:center; justify-content:space-between;
gap:8px; padding:10px 12px calc(10px + env(safe-area-inset-bottom));
background:rgba(28,28,30,.94);
-webkit-backdrop-filter:blur(12px);
backdrop-filter:blur(12px);
border-top:1px solid rgba(128,128,128,.3); }
.rm-nav a, .rm-nav span { font-size:14px; color:#eee !important;
text-decoration:none !important;
white-space:nowrap; }
.rm-nav a { padding:7px 13px; border-radius:99px;
border:1px solid rgba(160,160,160,.45); }
.rm-nav .rm-step { min-width:22px; text-align:center;
font-size:17px; line-height:1; padding:9px 12px; }
.rm-nav .rm-pos { opacity:.6; font-size:13px; }
.rm-nav .rm-pos-btn { opacity:1; padding:7px 12px; border-radius:99px;
border:1px solid rgba(160,160,160,.45);
cursor:pointer; }
.rm-nav .rm-off { opacity:.28; border-color:rgba(160,160,160,.2); }
.rm-sheet-bg { position:fixed; top:0; left:0; right:0; bottom:0;
z-index:10000; background:rgba(0,0,0,.5);
opacity:0; pointer-events:none; transition:opacity .18s; }
.rm-sheet-bg.rm-open { opacity:1; pointer-events:auto; }
.rm-sheet { box-sizing:border-box;
position:fixed; left:0; right:0; bottom:0; z-index:10001;
display:flex; flex-direction:column; max-height:72vh;
padding:8px 12px calc(12px + env(safe-area-inset-bottom));
color:#eee; background:rgba(28,28,30,.98);
-webkit-backdrop-filter:blur(14px);
backdrop-filter:blur(14px);
border-top:1px solid rgba(128,128,128,.3);
border-radius:14px 14px 0 0;
transform:translateY(102%);
transition:transform .22s ease; }
.rm-sheet.rm-open { transform:translateY(0); }
.rm-grip { flex:none; width:36px; height:4px; margin:2px auto 12px;
border-radius:2px; background:rgba(200,200,200,.35); }
.rm-goto { flex:none; display:flex; gap:8px; margin-bottom:12px; }
/* 16px verhindert, dass iOS beim Fokussieren in das Feld hineinzoomt. */
.rm-goto input { flex:1; min-width:0; box-sizing:border-box;
font-size:16px; padding:9px 12px; border-radius:10px;
color:#eee; background:rgba(120,120,120,.18);
border:1px solid rgba(160,160,160,.35); }
.rm-goto button { flex:none; font-size:14px; padding:9px 18px;
border-radius:10px; color:#eee;
background:rgba(120,120,120,.18);
border:1px solid rgba(160,160,160,.45); }
.rm-grid { flex:1; min-height:0; overflow-y:auto;
-webkit-overflow-scrolling:touch;
display:grid; gap:8px; padding-bottom:4px;
grid-template-columns:repeat(auto-fill, minmax(56px, 1fr)); }
.rm-grid a { display:flex; align-items:center; justify-content:center;
min-height:42px; font-size:14px; border-radius:10px;
color:#eee !important; text-decoration:none !important;
background:rgba(120,120,120,.12);
border:1px solid rgba(160,160,160,.3); }
.rm-grid a.rm-cur { font-weight:700;
background:rgba(120,170,255,.22);
border-color:rgba(120,170,255,.9); }
`;
h().appendChild(st);
function txt(el) { return (el ? el.textContent || '' : '')
.replace(/\s+/g, ' ').trim(); }
// Der Beitragstext liegt in einer verschachtelten Tabelle. closest('tr')
// trifft deshalb die innere Zeile, der Autor- und Profilspalte fehlen.
// Aufsteigen, bis die Zeile eine Profil- oder Autorenspalte enthaelt.
function postRow(pb) {
var tr = pb.closest('tr');
while (tr) {
if (tr.querySelector(':scope > td.profile') ||
tr.querySelector(':scope > td .postauthor')) return tr;
var up = tr.parentElement ? tr.parentElement.closest('tr') : null;
if (!up) return tr;
tr = up;
}
return null;
}
// --- Seitenzustand ---
// Der Block wird aus dem Original-DOM gelesen, bevor es ausgeblendet wird.
// phpBB zeigt nie alle Seitenzahlen an, deshalb wird nicht auf gerenderte
// Links gesetzt, sondern jede Ziel-URL aus start = (Seite - 1) * per_page
// selbst gebaut.
function pageState() {
var s = { cur: 1, total: 1, per: 0, prev: null, next: null };
// Der Paginierungsblock ist der einzige Ort, an dem Ziffern-Links
// Seitenzahlen bedeuten. Beitragstexte enthalten ebenfalls Links mit
// start=, die hier nicht mitgezaehlt werden duerfen.
var box = null;
var jumpA = document.querySelector('a[onclick*="jumpto"]');
if (jumpA) box = jumpA.parentElement;
if (!box) {
document.querySelectorAll('a[href*="start="]').forEach(function (a) {
if (!box && /^(Next|Weiter|Nächste)$/i.test(txt(a)))
box = a.parentElement;
});
}
// per_page steht als Literal in der jumpto()-Funktion der Seite.
document.querySelectorAll('script').forEach(function (el) {
if (s.per) return;
var m = (el.textContent || '').match(/per_page\s*=\s*'?(\d+)/);
if (m) s.per = parseInt(m[1], 10);
});
if (box) {
box.querySelectorAll('a[href*="start="]').forEach(function (a) {
var t = txt(a);
if (!/^\d+$/.test(t)) return;
var n = parseInt(t, 10);
if (n > s.total) s.total = n;
var ms = a.href.match(/[?&]start=(\d+)/);
if (!s.per && ms && n > 1) {
var per = parseInt(ms[1], 10) / (n - 1);
if (per > 0 && per === Math.floor(per)) s.per = per;
}
});
}
var cs = location.search.match(/[?&]start=(\d+)/);
if (s.per) s.cur = Math.floor((cs ? parseInt(cs[1], 10) : 0) / s.per) + 1;
else if (box) {
var strong = box.querySelector('strong, b > strong');
if (strong && /^\d+$/.test(txt(strong))) s.cur = parseInt(txt(strong), 10);
}
// Aeltere Themes geben stattdessen "Page X of Y" aus.
var pos = txt(document.body).match(/Page\s+(\d+)\s+of\s+(\d+)/i);
if (pos) {
if (!s.per) s.cur = parseInt(pos[1], 10);
s.total = Math.max(s.total, parseInt(pos[2], 10));
}
if (s.cur > s.total) s.total = s.cur;
s.url = function (n) {
if (n === s.cur || n < 1 || n > s.total) return null;
if (n !== 1 && !s.per) return null;
var u = location.href.split('#')[0];
var start = (n - 1) * s.per;
if (/[?&]start=\d+/.test(u))
return u.replace(/([?&]start=)\d+/, '$1' + start);
return u + (u.indexOf('?') === -1 ? '?' : '&') + 'start=' + start;
};
s.prev = s.url(s.cur - 1);
s.next = s.url(s.cur + 1);
// Ohne bekanntes per_page bleibt nur, die Original-Links zu uebernehmen.
if (!s.per) {
document.querySelectorAll('a').forEach(function (a) {
var t = txt(a);
if (!s.prev && /^(Previous|Vorherige)$/i.test(t)) s.prev = a.href;
if (!s.next && /^(Next|Weiter|Nächste)$/i.test(t)) s.next = a.href;
});
}
log.push('page=' + s.cur + '/' + s.total + ' per=' + s.per);
return s;
}
// Seitenwahl als Sheet ueber der Leiste. Wird erst beim ersten Antippen
// gebaut: bei 1000+ Seiten waere das Raster sonst umsonst im DOM.
function pageSheet(s) {
var bg = null, sheet = null;
function go(n) {
n = Math.min(Math.max(n, 1), s.total);
var u = s.url(n);
if (u) location.href = u;
else close();
}
function close() {
if (!bg) return;
bg.classList.remove('rm-open');
sheet.classList.remove('rm-open');
document.body.style.overflow = '';
}
function make() {
bg = document.createElement('div');
bg.className = 'rm-sheet-bg';
bg.addEventListener('click', close);
sheet = document.createElement('div');
sheet.className = 'rm-sheet';
var grip = document.createElement('div');
grip.className = 'rm-grip';
sheet.appendChild(grip);
var row = document.createElement('div');
row.className = 'rm-goto';
var inp = document.createElement('input');
inp.type = 'text';
inp.inputMode = 'numeric';
inp.pattern = '[0-9]*';
inp.placeholder = 'Seite 1 – ' + s.total;
inp.setAttribute('aria-label', 'Seitenzahl');
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Los';
function submit() {
var n = parseInt(inp.value, 10);
if (n > 0) go(n);
}
btn.addEventListener('click', submit);
inp.addEventListener('keydown', function (e) {
if (e.key === 'Enter') { e.preventDefault(); submit(); }
});
row.appendChild(inp);
row.appendChild(btn);
sheet.appendChild(row);
var grid = document.createElement('div');
grid.className = 'rm-grid';
var curEl = null;
for (var i = 1; i <= s.total; i++) {
var a = document.createElement('a');
a.textContent = i;
if (i === s.cur) { a.className = 'rm-cur'; curEl = a; }
else {
var u = s.url(i);
if (u) a.href = u;
}
grid.appendChild(a);
}
sheet.appendChild(grid);
document.body.appendChild(bg);
document.body.appendChild(sheet);
return curEl;
}
return function open() {
var curEl = bg ? null : make();
document.body.style.overflow = 'hidden';
// Ein Frame Abstand, sonst faellt der Uebergang beim ersten Oeffnen aus.
requestAnimationFrame(function () {
bg.classList.add('rm-open');
sheet.classList.add('rm-open');
if (curEl) curEl.scrollIntoView({ block: 'center' });
});
};
}
function build() {
var bodies = document.querySelectorAll('span.postbody, .postbody');
log.push('postbodies=' + bodies.length);
if (!bodies.length) return;
var root = document.createElement('div');
root.className = 'rm-root';
var tt = document.querySelector('a.maintitle, h1, .maintitle');
if (tt) {
var th = document.createElement('div');
th.className = 'rm-topic';
th.textContent = txt(tt);
root.appendChild(th);
}
var built = 0, seen = new Set();
bodies.forEach(function (pb) {
var row = postRow(pb);
if (!row || seen.has(row)) return;
seen.add(row);
var card = document.createElement('div');
card.className = 'rm-post';
// --- Zellen dieser Zeile + der Kopfzeilen darueber ---
var cells = [];
Array.prototype.forEach.call(row.children, function (td) {
if (!td.contains(pb)) cells.push(td);
});
var pr = row.previousElementSibling, g = 0;
while (pr && g++ < 3) {
Array.prototype.forEach.call(pr.children, function (td) {
cells.push(td);
});
if (pr.querySelector('.postauthor, a[href*="profile.php"]')) break;
pr = pr.previousElementSibling;
}
// --- Username ---
var user = '';
for (var i = 0; i < cells.length && !user; i++) {
var ua = cells[i].querySelector(
'.postauthor, .username-coloured, a[href*="profile.php"]');
if (ua) user = txt(ua);
}
if (!user) {
for (var i2 = 0; i2 < cells.length && !user; i2++) {
var nm = cells[i2].querySelector('span.name b, span.name, b.name');
if (nm) user = txt(nm);
}
}
user = user.replace(/^Post subject:.*/i, '').trim();
// --- Avatar ---
var avImg = null, bestArea = 0;
cells.forEach(function (td) {
td.querySelectorAll('img').forEach(function (im) {
var s = im.getAttribute('src') || '';
if (/spacer|blank|icon|rank|star|folder|button|pm|email|quote|edit|report|www|msn|aim|yim|icq|topic/i.test(s))
return;
if (/avatar/i.test(s)) { avImg = im; bestArea = 1e9; return; }
var w = parseInt(im.getAttribute('width'), 10) || im.naturalWidth || 0;
var hh = parseInt(im.getAttribute('height'), 10) || im.naturalHeight || 0;
var a = w * hh;
if (w >= 40 && a > bestArea) { avImg = im; bestArea = a; }
});
});
// --- Rang, Postzahl, Zeitstempel ---
var allTxt = cells.map(txt).join(' ');
var rank = '';
var rr = allTxt.match(/\b(Administrator|Site Admin|Global Moderator|Moderator|VIP|Uploader|Supporter|Member|Newbie|Guest)\b/i);
if (rr) rank = rr[1];
var pc = allTxt.match(/Posts:\s*([\d.,]+)/i);
if (pc) rank = (rank ? rank + ' · ' : '') + pc[1] + ' Posts';
var when = '';
var wm = allTxt.match(/Posted:\s*([^|]+?)(?:\s{2,}|Post subject|$)/i);
if (wm) when = wm[1].trim();
// --- Kopfzeile ---
var head = document.createElement('div');
head.className = 'rm-head';
var av;
if (avImg) {
av = document.createElement('img');
av.src = avImg.src;
} else {
av = document.createElement('div');
av.textContent = (user || '?').charAt(0).toUpperCase();
av.style.cssText = 'display:flex;align-items:center;' +
'justify-content:center;font-size:13px;font-weight:700;' +
'color:rgba(255,255,255,.75);';
}
av.className = 'rm-av';
head.appendChild(av);
var who = document.createElement('div');
who.className = 'rm-who';
var un = document.createElement('div');
un.className = 'rm-user';
un.textContent = user || 'Gast';
who.appendChild(un);
var sub = document.createElement('div');
sub.className = 'rm-sub';
sub.textContent = [rank, when].filter(Boolean).join(' · ');
who.appendChild(sub);
head.appendChild(who);
card.appendChild(head);
// --- Signatur abtrennen ---
var sig = null, hr = pb.querySelector('hr');
if (hr) {
sig = document.createElement('details');
sig.className = 'rm-sig';
var sm = document.createElement('summary');
sm.textContent = 'Signatur';
sig.appendChild(sm);
var n = hr.nextSibling;
while (n) { var nx = n.nextSibling; sig.appendChild(n); n = nx; }
hr.remove();
}
var bodyWrap = document.createElement('div');
bodyWrap.className = 'rm-body';
bodyWrap.appendChild(pb);
card.appendChild(bodyWrap);
if (sig && sig.childNodes.length > 1) card.appendChild(sig);
root.appendChild(card);
built++;
});
log.push('posts=' + built);
if (!built) return;
// --- Navigationsleiste ---
var pg = pageState();
var nav = document.createElement('div');
nav.className = 'rm-nav';
function navBtn(href, label, aria) {
var e = document.createElement('a');
e.className = 'rm-step' + (href ? '' : ' rm-off');
e.textContent = label;
e.setAttribute('aria-label', aria);
if (href) e.href = href;
return e;
}
nav.appendChild(navBtn(pg.url(1), '«', 'Erste Seite'));
nav.appendChild(navBtn(pg.prev, '‹', 'Vorherige Seite'));
var pos = document.createElement('span');
pos.className = 'rm-pos';
pos.textContent = pg.cur + ' / ' + pg.total;
// Ohne bekanntes per_page laesst sich kein Sprungziel bauen, dann bleibt
// die Anzeige reine Beschriftung.
if (pg.total > 1 && pg.per) {
pos.className += ' rm-pos-btn';
pos.textContent += ' ▾';
pos.setAttribute('role', 'button');
pos.setAttribute('aria-label', 'Seite wählen');
pos.addEventListener('click', pageSheet(pg));
}
nav.appendChild(pos);
nav.appendChild(navBtn(pg.next, '›', 'Nächste Seite'));
nav.appendChild(navBtn(pg.url(pg.total), '»', 'Letzte Seite'));
root.appendChild(nav);
Array.prototype.forEach.call(document.body.children, function (el) {
if (el !== root) el.classList.add('rm-hidden');
});
document.body.appendChild(root);
}
function run() {
try { build(); } catch (e) { log.push('build: ' + e.message); }
if (DEBUG) {
var d = document.createElement('div');
d.className = 'rm-dbg';
d.textContent = 'RIN Thread v2.3\n' + log.join('\n');
document.body.insertBefore(d, document.body.firstChild);
}
}
if (document.readyState === 'loading')
document.addEventListener('DOMContentLoaded', run);
else run();
})();