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
|
# CLAUDE.md
|
||||||
|
|
||||||
Zwei eigenstaendige Userscripts, die die phpBB-Seiten von `cs.rin.ru` fuer
|
Ein Userscript, das die phpBB-Seiten von `cs.rin.ru` fuer Safari auf iOS in eine
|
||||||
Safari auf iOS in eine mobile Ansicht umbauen. Kein Build, keine
|
mobile Ansicht umbaut. Kein Build, keine Abhaengigkeiten, kein `package.json` —
|
||||||
Abhaengigkeiten, kein `package.json` — die `.user.js`-Dateien sind das Produkt.
|
die `.user.js`-Datei ist das Produkt.
|
||||||
|
|
||||||
| Datei | Greift auf | Zweck |
|
| Datei | Greift auf | Zweck |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Themenlisten als Karten |
|
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Beide Ansichten, Weiche nach Seitentyp |
|
||||||
| `rin-thread.user.js` | `*://cs.rin.ru/forum/viewtopic.php*` | Thread als Beitragsstrom + Navigationsleiste |
|
| `dev/rin-mobile.dev.user.js` | zusaetzlich `localhost:*` | Loader, bindet per `@require file:///` das echte Script ein |
|
||||||
| `dev/*.dev.user.js` | zusaetzlich `localhost:*` | Loader, binden per `@require file:///` die echten Scripts ein |
|
|
||||||
| `dev/viewtopic.php` | — | Fixture: gespeicherter Thread, 1114 Seiten |
|
| `dev/viewtopic.php` | — | Fixture: gespeicherter Thread, 1114 Seiten |
|
||||||
|
|
||||||
Das Fixture ist ueber `.gitignore` (`dev/*.php`) ausgeschlossen. Nach einem
|
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
|
frischen Klon ist es nicht da und muss nach der Anleitung in der README neu
|
||||||
gespeichert werden — die Seite verlangt einen Login.
|
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
|
Ein IIFE, streng der Reihe nach:
|
||||||
anderen faellig:
|
|
||||||
|
|
||||||
1. IIFE mit `'use strict'`, `DEBUG`-Flag und `log`-Array ganz oben
|
1. `DEBUG`-Flag und `log`-Array
|
||||||
2. Viewport-Meta setzen (idempotent ueber `[data-rm]`)
|
2. Weiche: `view` ist `'thread'`, `'liste'` oder `''` — abgeleitet aus
|
||||||
3. Kompletter Style-Block als Template-Literal, **bedingungslos** injiziert
|
`location.pathname + location.search`
|
||||||
4. `build()` liest das Original-DOM, baut ein neues `div.rm-root`
|
3. Nur bei gesetztem `view`: Viewport-Meta (idempotent ueber `[data-rm]`) und
|
||||||
5. Alle bisherigen `body`-Kinder bekommen `.rm-hidden`, dann wird `root`
|
der komplette Style-Block als Template-Literal
|
||||||
angehaengt
|
4. `replaceBody(root)` blendet alle bisherigen `body`-Kinder aus und haengt die
|
||||||
6. `run()` haengt bei `DEBUG` das gruene Overlay oben an
|
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'`
|
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
|
## Fallen, die nicht aus dem Code hervorgehen
|
||||||
|
|
||||||
**Beide Stylesheets sind auf Threadseiten gleichzeitig aktiv.**
|
**Beide Ansichten teilen sich einen Klassennamensraum.** Das war der Grund fuer
|
||||||
`rin-mobile` matcht `*://cs.rin.ru/forum/*`, also auch `viewtopic.php`. Sein
|
die Zusammenlegung: als getrennte Scripts matchte `rin-mobile` auch
|
||||||
`build()` steigt dort zwar aus, der Style-Block wird aber vorher injiziert.
|
`viewtopic.php` und injizierte dort sein Stylesheet, obwohl es nichts baute.
|
||||||
Gleich benannte Klassen kollidieren mit identischer Spezifitaet, entschieden
|
Gleich benannte Regeln kollidierten mit identischer Spezifitaet, und die
|
||||||
wird nach Ladereihenfolge der Userscripts — und die ist nicht garantiert.
|
Ladereihenfolge der Userscripts entschied ueber das Layout.
|
||||||
|
|
||||||
- Nur bewusst geteilt: `.rm-root`, `.rm-hidden`, `.rm-dbg`
|
- Geteilt: `.rm-root`, `.rm-hidden`, `.rm-dbg`, die globale `img`-Regel
|
||||||
- **Offene Kollision:** `.rm-root` hat in `rin-mobile` `padding:0 !important`,
|
- Unterschiede zwischen den Ansichten ueber einen Modifier am Root loesen,
|
||||||
in `rin-thread` `padding:0 0 64px 0 !important`. Laedt `rin-mobile` spaeter,
|
nicht ueber zwei Regeln mit demselben Selektor. `.rm-root.rm-thread` haelt
|
||||||
faellt die Freihaltung hinter der fixierten Leiste weg. Noch nicht behoben.
|
so die 64px hinter der fixierten Leiste frei.
|
||||||
- Neue Klassen im Thread-Script gegen `rin-mobile.user.js` gegenpruefen.
|
- Namen sind ansichtsspezifisch zu halten: `.rm-jump`/`.rm-pages` gehoeren der
|
||||||
`.rm-goto` und `.rm-grid` heissen genau deshalb so — `.rm-jump` und
|
Liste, `.rm-goto`/`.rm-grid` dem Thread-Sheet.
|
||||||
`.rm-pages` waren dort schon belegt.
|
- `.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.**
|
**Was nach `body` soll, muss nach `replaceBody()` angehaengt werden.**
|
||||||
Die Schleife markiert *alle* vorhandenen `body`-Kinder mit `.rm-hidden`. Das
|
Die Funktion markiert *alle* vorhandenen `body`-Kinder mit `.rm-hidden`. Das
|
||||||
Sheet im Thread-Script wird deshalb erst beim ersten Antippen gebaut — das
|
Sheet wird deshalb erst beim ersten Antippen gebaut — das loest das Problem
|
||||||
loest das Problem nebenbei mit.
|
nebenbei mit.
|
||||||
|
|
||||||
**`Page X of Y` steht als `Page <strong>1</strong> of <strong>1114</strong>`
|
**`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
|
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
|
einer verschachtelten Tabelle, `closest('tr')` trifft die innere Zeile ohne
|
||||||
Autor- und Profilspalte.
|
Autor- und Profilspalte.
|
||||||
|
|
||||||
**`DEBUG` steht in beiden Dateien auf `true`** und muss vor dem Ausliefern in
|
**`DEBUG` steht auf `true`** und muss vor dem Ausliefern auf `false`. Die
|
||||||
beiden auf `false`. Die Versionsnummer steht ebenfalls doppelt: im
|
Versionsnummer steht doppelt: im `@version`-Header und im Text des
|
||||||
`@version`-Header und im Text des Debug-Overlays.
|
Debug-Overlays.
|
||||||
|
|
||||||
## Testen
|
## 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:
|
Degradation ohne `per_page` und ein Thread ohne Paginierung. Fixture-Eckdaten:
|
||||||
`f=10&t=68689`, `per_page = 15`, 1114 Seiten, letzte Seite `start=16695`.
|
`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-*)`,
|
Am Geraet zu pruefen bleibt, was Blink nicht zeigt: `env(safe-area-inset-*)`,
|
||||||
fixierte Leisten gegen Safaris ein- und ausfahrende Toolbar,
|
fixierte Leisten gegen Safaris ein- und ausfahrende Toolbar,
|
||||||
`-webkit-backdrop-filter`, Schriftskalierung. Details und die
|
`-webkit-backdrop-filter`, Schriftskalierung. Details und die
|
||||||
|
|||||||
@@ -6,14 +6,18 @@
|
|||||||
|
|
||||||
## English
|
## English
|
||||||
|
|
||||||
Two userscripts that rebuild the phpBB2 pages of `cs.rin.ru` into a mobile card
|
A single userscript that rebuilds the phpBB2 pages of `cs.rin.ru` into a mobile
|
||||||
layout for Safari on iOS. Both replace the original table layout entirely: the
|
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>`.
|
existing page content is hidden and a freshly built DOM is appended to `<body>`.
|
||||||
|
|
||||||
| Script | Matches | Purpose |
|
`rin-mobile.user.js` matches `*://cs.rin.ru/forum/*` and picks its view from the
|
||||||
|---|---|---|
|
page type:
|
||||||
| `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 |
|
| 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
|
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
|
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
|
page; without it, only first page and the original previous/next links remain
|
||||||
available.
|
available.
|
||||||
|
|
||||||
Both scripts run at `document-start`, inject a mobile viewport meta tag, and
|
The script runs at `document-start`, injects a mobile viewport meta tag, and
|
||||||
carry a `DEBUG` flag. While `DEBUG` is `true`, a green diagnostic overlay is
|
carries 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
|
prepended to the page showing which view was chosen and how many rows were
|
||||||
`false` in both files before daily use.
|
converted. Set it to `false` before daily use.
|
||||||
|
|
||||||
### Installation on iOS
|
### Installation on iOS
|
||||||
|
|
||||||
1. Install a userscript manager for Safari on iOS (for example *Userscripts*)
|
1. Install a userscript manager for Safari on iOS (for example *Userscripts*)
|
||||||
and enable it under *Settings → Safari → Extensions*.
|
and enable it under *Settings → Safari → Extensions*.
|
||||||
2. Add `rin-mobile.user.js` and `rin-thread.user.js` to the manager's script
|
2. Add `rin-mobile.user.js` to the manager's script directory.
|
||||||
directory.
|
|
||||||
3. Grant the extension permission for `cs.rin.ru`.
|
3. Grant the extension permission for `cs.rin.ru`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
The scripts are developed on Windows in Chrome or Edge with Tampermonkey. The
|
The script is developed on Windows in Chrome or Edge with Tampermonkey.
|
||||||
files in `dev/` are loaders: they contain nothing but a metadata block that
|
`dev/rin-mobile.dev.user.js` is a loader: it contains nothing but a metadata
|
||||||
pulls in the real script from disk via `@require`. Editing
|
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 —
|
`rin-mobile.user.js` and reloading the page is enough — no reinstall in
|
||||||
no reinstall in Tampermonkey.
|
Tampermonkey.
|
||||||
|
|
||||||
#### ⚠️ Path adjustment required
|
#### ⚠️ Path adjustment required
|
||||||
|
|
||||||
The loaders in `dev/` reference the scripts by **absolute path**:
|
The loader in `dev/` references the script by **absolute path**:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.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
|
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
|
cloning the repository or moving the project folder, adjust the `@require` line
|
||||||
in **both** loaders to your own location:
|
in `dev/rin-mobile.dev.user.js` to your own location.
|
||||||
|
|
||||||
- `dev/rin-mobile.dev.user.js`
|
|
||||||
- `dev/rin-thread.dev.user.js`
|
|
||||||
|
|
||||||
Use forward slashes and the `file:///` prefix, including the drive letter —
|
Use forward slashes and the `file:///` prefix, including the drive letter —
|
||||||
`file:///D:/projects/ios_safari_userscripts/rin-mobile.user.js`. A `@require`
|
`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
|
to **always**, otherwise the loader keeps serving a cached copy of the
|
||||||
script.
|
script.
|
||||||
|
|
||||||
Then open the loaders once via `file:///.../dev/rin-mobile.dev.user.js` —
|
Then open the loader once via `file:///.../dev/rin-mobile.dev.user.js` —
|
||||||
Tampermonkey intercepts `.user.js` URLs and offers to install them.
|
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
|
Do not run the loader and the plain script side by side. If both are enabled in
|
||||||
in Tampermonkey, the code executes twice on the same page, producing duplicate
|
Tampermonkey, the code executes twice on the same page, producing duplicate
|
||||||
cards and two debug overlays.
|
cards and two debug overlays.
|
||||||
|
|
||||||
#### Test fixtures
|
#### Test fixtures
|
||||||
@@ -100,11 +100,11 @@ against local copies:
|
|||||||
|
|
||||||
The file name matters twice over. The `.html` extension makes the server send
|
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
|
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.
|
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
|
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.
|
empty, since their `src` is carried over unchanged.
|
||||||
|
|
||||||
Note that a snapshot saved while logged out contains `span.link_removed`
|
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
|
### Repository layout
|
||||||
|
|
||||||
```
|
```
|
||||||
rin-mobile.user.js topic list → cards
|
rin-mobile.user.js topic list → cards, thread → post stream
|
||||||
rin-thread.user.js thread → post stream
|
|
||||||
dev/
|
dev/
|
||||||
rin-mobile.dev.user.js loader, @require with absolute path
|
rin-mobile.dev.user.js loader, @require with absolute path
|
||||||
rin-thread.dev.user.js loader, @require with absolute path
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deutsch
|
## Deutsch
|
||||||
|
|
||||||
Zwei Userscripts, die die phpBB2-Seiten von `cs.rin.ru` für Safari auf iOS in
|
Ein Userscript, das die phpBB2-Seiten von `cs.rin.ru` für Safari auf iOS in eine
|
||||||
eine mobile Kartenansicht umbauen. Beide ersetzen das ursprüngliche
|
mobile Ansicht umbaut. Es ersetzt das ursprüngliche Tabellenlayout vollständig:
|
||||||
Tabellenlayout vollständig: der vorhandene Seiteninhalt wird ausgeblendet und
|
der vorhandene Seiteninhalt wird ausgeblendet und ein neu gebautes DOM an
|
||||||
ein neu gebautes DOM an `<body>` gehängt.
|
`<body>` gehängt.
|
||||||
|
|
||||||
| Script | Greift auf | Zweck |
|
`rin-mobile.user.js` greift auf `*://cs.rin.ru/forum/*` und wählt die Ansicht
|
||||||
|---|---|---|
|
anhand des Seitentyps:
|
||||||
| `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 |
|
| 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
|
Die Navigationsleiste im Thread enthält erste / vorherige / nächste / letzte
|
||||||
Seite und eine Positionsanzeige. Ein Tipp darauf öffnet ein Sheet mit
|
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
|
einbettet; fehlt sie, bleiben nur die erste Seite und die ursprünglichen
|
||||||
Zurück-/Weiter-Links nutzbar.
|
Zurück-/Weiter-Links nutzbar.
|
||||||
|
|
||||||
Beide laufen bei `document-start`, setzen ein mobiles Viewport-Meta-Tag und
|
Das Script läuft bei `document-start`, setzt ein mobiles Viewport-Meta-Tag und
|
||||||
haben ein `DEBUG`-Flag. Solange `DEBUG` auf `true` steht, wird ein grünes
|
hat 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
|
Diagnose-Overlay über die Seite gelegt, das zeigt, welche Ansicht gewählt wurde
|
||||||
umgewandelt wurden. Für den täglichen Gebrauch in beiden Dateien auf `false`
|
und wie viele Zeilen umgewandelt wurden. Für den täglichen Gebrauch auf `false`
|
||||||
setzen.
|
setzen.
|
||||||
|
|
||||||
### Installation unter iOS
|
### Installation unter iOS
|
||||||
|
|
||||||
1. Einen Userscript-Manager für Safari unter iOS installieren (zum Beispiel
|
1. Einen Userscript-Manager für Safari unter iOS installieren (zum Beispiel
|
||||||
*Userscripts*) und unter *Einstellungen → Safari → Erweiterungen* aktivieren.
|
*Userscripts*) und unter *Einstellungen → Safari → Erweiterungen* aktivieren.
|
||||||
2. `rin-mobile.user.js` und `rin-thread.user.js` in das Script-Verzeichnis des
|
2. `rin-mobile.user.js` in das Script-Verzeichnis des Managers legen.
|
||||||
Managers legen.
|
|
||||||
3. Der Erweiterung die Berechtigung für `cs.rin.ru` erteilen.
|
3. Der Erweiterung die Berechtigung für `cs.rin.ru` erteilen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Entwicklung
|
### Entwicklung
|
||||||
|
|
||||||
Entwickelt wird unter Windows in Chrome oder Edge mit Tampermonkey. Die Dateien
|
Entwickelt wird unter Windows in Chrome oder Edge mit Tampermonkey.
|
||||||
in `dev/` sind Loader: sie enthalten nur einen Metadatenblock, der das
|
`dev/rin-mobile.dev.user.js` ist ein Loader: er enthält nur einen
|
||||||
eigentliche Script per `@require` von der Platte einbindet. Es genügt,
|
Metadatenblock, der das eigentliche Script per `@require` von der Platte
|
||||||
`rin-mobile.user.js` oder `rin-thread.user.js` zu bearbeiten und die Seite neu
|
einbindet. Es genügt, `rin-mobile.user.js` zu bearbeiten und die Seite neu zu
|
||||||
zu laden — keine Neuinstallation in Tampermonkey nötig.
|
laden — keine Neuinstallation in Tampermonkey nötig.
|
||||||
|
|
||||||
#### ⚠️ Pfadanpassung erforderlich
|
#### ⚠️ 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
|
```js
|
||||||
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.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
|
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
|
geschrieben wurde. Nach dem Klonen des Repositories oder dem Verschieben des
|
||||||
Projektordners muss die `@require`-Zeile in **beiden** Loadern auf den eigenen
|
Projektordners muss die `@require`-Zeile in `dev/rin-mobile.dev.user.js` auf den
|
||||||
Ablageort angepasst werden:
|
eigenen Ablageort angepasst werden.
|
||||||
|
|
||||||
- `dev/rin-mobile.dev.user.js`
|
|
||||||
- `dev/rin-thread.dev.user.js`
|
|
||||||
|
|
||||||
Dabei Schrägstriche und das Präfix `file:///` inklusive Laufwerksbuchstaben
|
Dabei Schrägstriche und das Präfix `file:///` inklusive Laufwerksbuchstaben
|
||||||
verwenden — `file:///D:/projekte/ios_safari_userscripts/rin-mobile.user.js`.
|
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
|
**immer** stellen, sonst liefert der Loader weiterhin eine zwischen
|
||||||
gespeicherte Fassung des Scripts aus.
|
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.
|
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
|
Tampermonkey aktiv, läuft der Code doppelt auf derselben Seite, mit doppelten
|
||||||
Karten und zwei Debug-Overlays als Folge.
|
Karten und zwei Debug-Overlays als Folge.
|
||||||
|
|
||||||
@@ -291,9 +289,7 @@ zwischenspeichert:
|
|||||||
### Aufbau des Repositories
|
### Aufbau des Repositories
|
||||||
|
|
||||||
```
|
```
|
||||||
rin-mobile.user.js Themenliste → Karten
|
rin-mobile.user.js Themenliste → Karten, Thread → Beitragsstrom
|
||||||
rin-thread.user.js Thread → Beitragsstrom
|
|
||||||
dev/
|
dev/
|
||||||
rin-mobile.dev.user.js Loader, @require mit absolutem Pfad
|
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
|
// @name [DEV] RIN Mobile
|
||||||
// @namespace rin-dev
|
// @namespace rin-dev
|
||||||
// @description Dev-Loader: laedt rin-mobile.user.js live von der Platte
|
// @description Dev-Loader: laedt rin-mobile.user.js live von der Platte
|
||||||
// @version 1.0
|
// @version 2.0
|
||||||
// @match *://cs.rin.ru/forum/*
|
// @match *://cs.rin.ru/forum/*
|
||||||
// @include http://localhost:*/*viewforum*
|
// @include http://localhost:*/*viewforum*
|
||||||
// @include http://127.0.0.1:*/*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
|
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.js
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// @grant none
|
// @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.
|
|
||||||
+529
-27
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name RIN Mobile
|
// @name RIN Mobile
|
||||||
// @description Mobile Karten-Ansicht fuer cs.rin.ru
|
// @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom
|
||||||
// @version 1.4
|
// @version 3.0
|
||||||
// @match *://cs.rin.ru/forum/*
|
// @match *://cs.rin.ru/forum/*
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
@@ -14,7 +14,22 @@
|
|||||||
|
|
||||||
function h() { return document.head || document.documentElement; }
|
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');
|
var m = document.createElement('meta');
|
||||||
m.name = 'viewport';
|
m.name = 'viewport';
|
||||||
m.content = 'width=device-width, initial-scale=1, viewport-fit=cover';
|
m.content = 'width=device-width, initial-scale=1, viewport-fit=cover';
|
||||||
@@ -22,8 +37,11 @@
|
|||||||
h().appendChild(m);
|
h().appendChild(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
var st = document.createElement('style');
|
if (view) {
|
||||||
st.textContent = `
|
var st = document.createElement('style');
|
||||||
|
st.textContent = `
|
||||||
|
/* === Grundlage, beide Ansichten === */
|
||||||
|
|
||||||
html, body { max-width:100% !important; overflow-x:hidden !important;
|
html, body { max-width:100% !important; overflow-x:hidden !important;
|
||||||
margin:0 !important; padding:0 !important; }
|
margin:0 !important; padding:0 !important; }
|
||||||
body { font-size:16px !important;
|
body { font-size:16px !important;
|
||||||
@@ -33,12 +51,17 @@
|
|||||||
.rm-dbg { background:#222; color:#0f0; font:12px/1.45 monospace;
|
.rm-dbg { background:#222; color:#0f0; font:12px/1.45 monospace;
|
||||||
padding:8px 10px; white-space:pre-wrap;
|
padding:8px 10px; white-space:pre-wrap;
|
||||||
border-bottom:2px solid #0f0; }
|
border-bottom:2px solid #0f0; }
|
||||||
|
.rm-hidden { display:none !important; }
|
||||||
|
|
||||||
.rm-root { box-sizing:border-box !important;
|
.rm-root { box-sizing:border-box !important; display:block !important;
|
||||||
display:block !important;
|
|
||||||
width:100vw !important; max-width:100vw !important;
|
width:100vw !important; max-width:100vw !important;
|
||||||
margin:0 !important; padding:0 !important;
|
margin:0 !important; padding:0 !important;
|
||||||
overflow:hidden !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;
|
.rm-card { box-sizing:border-box !important;
|
||||||
display:block !important;
|
display:block !important;
|
||||||
width:100% !important; max-width:100% !important;
|
width:100% !important; max-width:100% !important;
|
||||||
@@ -76,11 +99,132 @@
|
|||||||
.rm-pages a { padding:6px 11px; border-radius:8px;
|
.rm-pages a { padding:6px 11px; border-radius:8px;
|
||||||
border:1px solid rgba(128,128,128,.35);
|
border:1px solid rgba(128,128,128,.35);
|
||||||
text-decoration:none !important; color:inherit !important; }
|
text-decoration:none !important; color:inherit !important; }
|
||||||
.rm-hidden { display:none !important; }
|
|
||||||
`;
|
|
||||||
h().appendChild(st);
|
|
||||||
|
|
||||||
function txt(el) { return (el.textContent || '').replace(/\s+/g, ' ').trim(); }
|
/* === Thread === */
|
||||||
|
|
||||||
|
.rm-topic { box-sizing:border-box; padding:16px 14px 12px;
|
||||||
|
font-size:21px; font-weight:650; line-height:1.3;
|
||||||
|
overflow-wrap:break-word; }
|
||||||
|
|
||||||
|
.rm-post { box-sizing:border-box !important;
|
||||||
|
width:100% !important; max-width:100% !important;
|
||||||
|
padding:14px !important;
|
||||||
|
border-top:1px solid rgba(128,128,128,.25) !important; }
|
||||||
|
|
||||||
|
.rm-head { display:flex; align-items:center; gap:9px;
|
||||||
|
margin-bottom:10px; min-width:0; }
|
||||||
|
/* Die Masse muessen sich gegen die globale img-Regel durchsetzen. */
|
||||||
|
.rm-av { flex:none; width:28px !important; height:28px !important;
|
||||||
|
border-radius:50%; object-fit:cover;
|
||||||
|
background:rgba(128,128,128,.25); }
|
||||||
|
.rm-who { min-width:0; flex:1; }
|
||||||
|
.rm-user { font-size:14px; font-weight:650; line-height:1.2;
|
||||||
|
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
|
.rm-sub { font-size:11px; opacity:.55; line-height:1.3;
|
||||||
|
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
|
|
||||||
|
.rm-body { font-size:17px !important; line-height:1.55 !important;
|
||||||
|
overflow-wrap:break-word !important;
|
||||||
|
word-break:normal !important; }
|
||||||
|
.rm-body * { max-width:100% !important; }
|
||||||
|
.rm-body img { height:auto !important; border-radius:6px; }
|
||||||
|
.rm-body a { overflow-wrap:anywhere !important; }
|
||||||
|
.rm-body table { display:block !important; overflow-x:auto !important;
|
||||||
|
width:auto !important; }
|
||||||
|
.rm-body pre, .rm-body code {
|
||||||
|
display:block; overflow-x:auto; white-space:pre;
|
||||||
|
font-size:13px; padding:10px; border-radius:8px;
|
||||||
|
background:rgba(128,128,128,.14); }
|
||||||
|
.rm-body blockquote, .rm-body .quote, .rm-body table.quote {
|
||||||
|
display:block !important; margin:12px 0 !important;
|
||||||
|
padding:8px 0 8px 12px !important;
|
||||||
|
border-left:3px solid rgba(140,140,255,.5) !important;
|
||||||
|
font-size:15px !important; opacity:.8 !important;
|
||||||
|
background:none !important; }
|
||||||
|
.rm-body ul, .rm-body ol { padding-left:22px !important; }
|
||||||
|
.rm-body hr { border:0; border-top:1px solid rgba(128,128,128,.3); }
|
||||||
|
|
||||||
|
.rm-sig { margin-top:12px; font-size:13px; opacity:.5; }
|
||||||
|
.rm-sig summary { cursor:pointer; opacity:.8; }
|
||||||
|
|
||||||
|
.rm-nav { position:fixed; left:0; right:0; bottom:0; z-index:9999;
|
||||||
|
display:flex; align-items:center; justify-content:space-between;
|
||||||
|
gap:8px; padding:10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||||
|
background:rgba(28,28,30,.94);
|
||||||
|
-webkit-backdrop-filter:blur(12px);
|
||||||
|
backdrop-filter:blur(12px);
|
||||||
|
border-top:1px solid rgba(128,128,128,.3); }
|
||||||
|
.rm-nav a, .rm-nav span { font-size:14px; color:#eee !important;
|
||||||
|
text-decoration:none !important;
|
||||||
|
white-space:nowrap; }
|
||||||
|
.rm-nav a { padding:7px 13px; border-radius:99px;
|
||||||
|
border:1px solid rgba(160,160,160,.45); }
|
||||||
|
.rm-nav .rm-step { min-width:22px; text-align:center;
|
||||||
|
font-size:17px; line-height:1; padding:9px 12px; }
|
||||||
|
.rm-nav .rm-pos { opacity:.6; font-size:13px; }
|
||||||
|
.rm-nav .rm-pos-btn { opacity:1; padding:7px 12px; border-radius:99px;
|
||||||
|
border:1px solid rgba(160,160,160,.45);
|
||||||
|
cursor:pointer; }
|
||||||
|
.rm-nav .rm-off { opacity:.28; border-color:rgba(160,160,160,.2); }
|
||||||
|
|
||||||
|
.rm-sheet-bg { position:fixed; top:0; left:0; right:0; bottom:0;
|
||||||
|
z-index:10000; background:rgba(0,0,0,.5);
|
||||||
|
opacity:0; pointer-events:none; transition:opacity .18s; }
|
||||||
|
.rm-sheet-bg.rm-open { opacity:1; pointer-events:auto; }
|
||||||
|
|
||||||
|
.rm-sheet { box-sizing:border-box;
|
||||||
|
position:fixed; left:0; right:0; bottom:0; z-index:10001;
|
||||||
|
display:flex; flex-direction:column; max-height:72vh;
|
||||||
|
padding:8px 12px calc(12px + env(safe-area-inset-bottom));
|
||||||
|
color:#eee; background:rgba(28,28,30,.98);
|
||||||
|
-webkit-backdrop-filter:blur(14px);
|
||||||
|
backdrop-filter:blur(14px);
|
||||||
|
border-top:1px solid rgba(128,128,128,.3);
|
||||||
|
border-radius:14px 14px 0 0;
|
||||||
|
transform:translateY(102%);
|
||||||
|
transition:transform .22s ease; }
|
||||||
|
.rm-sheet.rm-open { transform:translateY(0); }
|
||||||
|
.rm-grip { flex:none; width:36px; height:4px; margin:2px auto 12px;
|
||||||
|
border-radius:2px; background:rgba(200,200,200,.35); }
|
||||||
|
.rm-goto { flex:none; display:flex; gap:8px; margin-bottom:12px; }
|
||||||
|
/* 16px verhindert, dass iOS beim Fokussieren in das Feld hineinzoomt. */
|
||||||
|
.rm-goto input { flex:1; min-width:0; box-sizing:border-box;
|
||||||
|
font-size:16px; padding:9px 12px; border-radius:10px;
|
||||||
|
color:#eee; background:rgba(120,120,120,.18);
|
||||||
|
border:1px solid rgba(160,160,160,.35); }
|
||||||
|
.rm-goto button { flex:none; font-size:14px; padding:9px 18px;
|
||||||
|
border-radius:10px; color:#eee;
|
||||||
|
background:rgba(120,120,120,.18);
|
||||||
|
border:1px solid rgba(160,160,160,.45); }
|
||||||
|
.rm-grid { flex:1; min-height:0; overflow-y:auto;
|
||||||
|
-webkit-overflow-scrolling:touch;
|
||||||
|
display:grid; gap:8px; padding-bottom:4px;
|
||||||
|
grid-template-columns:repeat(auto-fill, minmax(56px, 1fr)); }
|
||||||
|
.rm-grid a { display:flex; align-items:center; justify-content:center;
|
||||||
|
min-height:42px; font-size:14px; border-radius:10px;
|
||||||
|
color:#eee !important; text-decoration:none !important;
|
||||||
|
background:rgba(120,120,120,.12);
|
||||||
|
border:1px solid rgba(160,160,160,.3); }
|
||||||
|
.rm-grid a.rm-cur { font-weight:700;
|
||||||
|
background:rgba(120,170,255,.22);
|
||||||
|
border-color:rgba(120,170,255,.9); }
|
||||||
|
`;
|
||||||
|
h().appendChild(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Originalinhalt ausblenden und die neue Ansicht als einziges sichtbares
|
||||||
|
// body-Kind einhaengen. Alles, was spaeter noch an body soll, muss nach
|
||||||
|
// diesem Aufruf kommen.
|
||||||
|
function replaceBody(root) {
|
||||||
|
Array.prototype.forEach.call(document.body.children, function (el) {
|
||||||
|
if (el !== root) el.classList.add('rm-hidden');
|
||||||
|
});
|
||||||
|
document.body.appendChild(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Themenliste
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
function breakable(s) {
|
function breakable(s) {
|
||||||
return s.replace(/([|\/\\,;:>\]\)])/g, '$1\u200B');
|
return s.replace(/([|\/\\,;:>\]\)])/g, '$1\u200B');
|
||||||
@@ -156,13 +300,7 @@
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
function build() {
|
function buildList() {
|
||||||
var onList = /viewforum\.php|search\.php/
|
|
||||||
.test(location.pathname + location.search) ||
|
|
||||||
location.pathname === '/forum/';
|
|
||||||
log.push('listenseite=' + onList);
|
|
||||||
if (!onList) return;
|
|
||||||
|
|
||||||
var links = document.querySelectorAll(
|
var links = document.querySelectorAll(
|
||||||
'a.topictitle,a[href*="viewtopic.php"]');
|
'a.topictitle,a[href*="viewtopic.php"]');
|
||||||
log.push('themenlinks=' + links.length);
|
log.push('themenlinks=' + links.length);
|
||||||
@@ -205,11 +343,7 @@
|
|||||||
});
|
});
|
||||||
if (pages.children.length) root.appendChild(pages);
|
if (pages.children.length) root.appendChild(pages);
|
||||||
|
|
||||||
// Originalinhalt ausblenden, Liste als direktes body-Kind einhaengen
|
replaceBody(root);
|
||||||
Array.prototype.forEach.call(document.body.children, function (el) {
|
|
||||||
if (el !== root) el.classList.add('rm-hidden');
|
|
||||||
});
|
|
||||||
document.body.appendChild(root);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
var c0 = root.querySelector('.rm-card');
|
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() {
|
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) {
|
if (DEBUG) {
|
||||||
var d = document.createElement('div');
|
var d = document.createElement('div');
|
||||||
d.className = 'rm-dbg';
|
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);
|
document.body.insertBefore(d, document.body.firstChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === 'loading')
|
if (document.readyState === 'loading')
|
||||||
document.addEventListener('DOMContentLoaded', run);
|
document.addEventListener('DOMContentLoaded', run);
|
||||||
else
|
else run();
|
||||||
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