mirror of
https://github.com/Andre-Bzk/rin-mobile-userscript.git
synced 2026-11-15 09:33:08 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
})();
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
Reference in New Issue
Block a user