mirror of
https://github.com/Andre-Bzk/rin-mobile-userscript.git
synced 2026-11-15 09:33:08 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e459973149 | ||
|
|
2ea94f8a2a | ||
|
|
c477537826 | ||
|
|
e7f3f56456 | ||
|
|
a8ca211805 | ||
|
|
21820b88d0 |
@@ -1,23 +1,38 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Ein Userscript, das die phpBB-Seiten von `cs.rin.ru` fuer Safari auf iOS in eine
|
||||
Ein Userscript, das die phpBB3-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/*` | 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 |
|
||||
| `dev/viewforum.php` | — | Fixture: Main Forum, 610 Seiten, mit Unterforen, Ankuendigungen und Stickies |
|
||||
| `dev/index.php` | — | Fixture: Forumwurzel, zwei Kategorien mit Forenzeilen, keine Paginierung |
|
||||
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Alle Ansichten, Weiche nach Seitentyp |
|
||||
| `dev/rin-mobile.dev.user.js` | `*://cs.rin.ru/forum/*` | Loader, bindet per `@require file:///` das echte Script ein |
|
||||
| `dev/viewtopic.php` | — | Fixture: gespeicherter Thread, `per_page=15`, 1114 Seiten |
|
||||
| `dev/viewforum.php` | — | Fixture: Main Forum, `per_page=100`, 610 Seiten, mit Unterforen, Ankuendigungen und Stickies |
|
||||
| `dev/index.php` | — | Fixture: Forumwurzel, zwei Kategorien mit Forenzeilen, keine Paginierung, eingeloggt aufgenommen |
|
||||
| `dev/ucp.php` | — | Fixture: Login-Formular (`mode=login`), nur ausgeloggter Zustand |
|
||||
| `dev/after_login/ucp.php` | — | Fixture: dieselbe URL nach erfolgreichem Login, "Information"-Vorlage statt Formular |
|
||||
| `dev/search.php` | — | Fixture: Suchformular ohne Treffer, eingeloggt aufgenommen |
|
||||
| `dev/search2.php` | — | Fixture: Suchtreffer im Modus "Posts" (15 von 1438 Treffern, `t=63042`, `per_page=15`, 96 Seiten) |
|
||||
|
||||
Die Fixtures sind ueber `.gitignore` (`dev/*.php`) ausgeschlossen. Nach einem
|
||||
frischen Klon sind sie nicht da und muessen nach der Anleitung in der README neu
|
||||
gespeichert werden — die Seite verlangt einen Login.
|
||||
|
||||
Der Loader greift ausschliesslich auf die echte Seite. Die Fixtures sind
|
||||
Markup-Vorlagen fuer das jsdom-Harness und zum Nachlesen, keine Seiten zum
|
||||
Anschauen im Browser — die Layoutarbeit laeuft in Chrome auf `cs.rin.ru`
|
||||
selbst. Sie lokal auszuliefern verlangte `@include`-Regeln fuer `localhost`,
|
||||
und die passen auf Seitennamen, die dort haeufig genug vorkommen (`index.php`,
|
||||
alles mit `search` darin), dass der globale Teil des Stylesheets
|
||||
(`html, body`, `img`, jeweils `!important`) in fremde lokale Projekte geriete.
|
||||
Das Nachziehen waere billig, der Fehlersuchweg dorthin nicht.
|
||||
|
||||
Fuer den Zustand nach erfolgreichem Login (`ucp.php?mode=login` ohne Formular,
|
||||
"Information"-Vorlage) liegt derzeit keine Fixture im Repo. `buildLoginInfo()`
|
||||
wurde gegen einen Mitschnitt gebaut, der nicht mehr vorhanden ist; wer den
|
||||
Zweig anfassen will, muss die Seite neu aufnehmen.
|
||||
|
||||
## Aufbau
|
||||
|
||||
Ein IIFE, streng der Reihe nach:
|
||||
@@ -32,9 +47,20 @@ Ein IIFE, streng der Reihe nach:
|
||||
5. `pageState()`, `pageSheet()` und `pageNav()` — die Paginierung, von Liste
|
||||
und Thread benutzt, nicht vom Login
|
||||
6. `buildList()`, `buildThread()` beziehungsweise `buildLogin()` lesen das
|
||||
Original-DOM und bauen ein `div.rm-root`; alle drei haengen zum Schluss
|
||||
Original-DOM und bauen ein `div.rm-root`; alle haengen zum Schluss
|
||||
ueber `addCrumbs()` die fixierte Brotkrumenleiste an, die zugleich ueber
|
||||
`loginState()` die Login/Logout-Pille traegt
|
||||
`loginState()` die Login/Logout-Pille traegt. Zwei der drei Ansichten
|
||||
verzweigen intern weiter, weil sich mehrere Seitenformen dieselbe URL
|
||||
teilen:
|
||||
- `buildList()` versucht zuerst `buildSearchForm()` (grosses Suchformular
|
||||
auf `search.php`), dann `buildSearchResults()` (Trefferliste im Modus
|
||||
"Posts", Karten ueber `buildHit()`); erst wenn beide `false` liefern,
|
||||
laeuft das gewoehnliche Zeileneinsammeln
|
||||
- `buildLogin()` faellt ohne Formular auf `buildLoginInfo()` zurueck
|
||||
("Information"-Vorlage nach dem Absenden)
|
||||
- `buildThread()` legt ueber `buildThreadSearch()` die zweite Leiste an,
|
||||
die die Lupe in den Brotkrumen per `toggleThreadSearch()` ein- und
|
||||
ausblendet
|
||||
7. `run()` waehlt die Ansicht und haengt bei `DEBUG` das gruene Overlay an
|
||||
8. Start ueber `DOMContentLoaded`, falls `readyState === 'loading'`
|
||||
|
||||
@@ -123,7 +149,10 @@ Titelzelle gebildet, nie aus deren gesamtem Text.
|
||||
Klassennamen aus der phpBB-Dokumentation (`forumlink`, `forumdesc`) deshalb
|
||||
zunaechst nur eine Vermutung; `dev/viewforum.php` bestaetigt sie inzwischen. Der
|
||||
Rueckfall auf die Form der Zeile bleibt trotzdem stehen — er kostet nichts und
|
||||
traegt die Erkennung, wenn das Theme wechselt.
|
||||
traegt die Erkennung, wenn das Theme wechselt. `rinDark` ist allerdings eine
|
||||
Ableitung von subsilver2 und laedt dessen
|
||||
`styles/subsilver2/template/forum_fn.js` mit — Klassennamen aus subsilver2 sind
|
||||
deshalb eine brauchbare erste Annahme, wenn eine Seite noch keine Fixture hat.
|
||||
|
||||
**Die Brotkrumenleiste ist fixiert, nicht sticky.** `.rm-root` traegt
|
||||
`overflow:hidden` und wuerde damit zum Scrollport einer `position:sticky`
|
||||
@@ -135,8 +164,9 @@ Brotkrumen im Markup kein leerer Streifen bleibt.
|
||||
**Die Login-Erkennung ist inzwischen gegen eine echte Sitzung geprueft.**
|
||||
`loginState()` unterscheidet `a[href*="mode=login"]` von
|
||||
`a[href*="mode=logout"]` im Menuebalken und liest den Namen aus dem Muster
|
||||
`Logout [ Name ]` im Linktext. `dev/index.php` und `dev/after_login/ucp.php`
|
||||
sind eingeloggt aufgenommen (`Logout [ <Benutzername> ]`) und bestaetigen das Muster.
|
||||
`Logout [ Name ]` im Linktext. `dev/index.php`, `dev/search.php` und
|
||||
`dev/search2.php` sind eingeloggt aufgenommen (`Logout [ <Benutzername> ]`) und
|
||||
bestaetigen das Muster.
|
||||
Der Fallback bleibt trotzdem stehen: passt das Muster einmal nicht — andere
|
||||
Sprache, anderer Klammertyp — zeigt die Pille „Logout" ohne Namen, statt eine
|
||||
Ausnahme zu werfen.
|
||||
@@ -149,7 +179,9 @@ liegt aber keine Fixture vor). `buildLoginInfo()` erkennt die Vorlage deshalb
|
||||
an ihrer Form — eine Kopfzeile mit genau einer `th`-Zelle, gefolgt von einer
|
||||
`td.row1`-Nachrichtenzelle — nicht am Wortlaut, und uebernimmt die Zelle
|
||||
unveraendert wie das Formular selbst. Findet `buildLogin()` weder Formular
|
||||
noch diese Form, baut es nichts; die Originalseite bleibt sichtbar.
|
||||
noch diese Form, baut es nichts; die Originalseite bleibt sichtbar. Achtung:
|
||||
die Fixture, gegen die das gebaut wurde, liegt nicht mehr im Repo — der Zweig
|
||||
ist derzeit nicht nachpruefbar, ohne die Seite neu aufzunehmen.
|
||||
|
||||
**`index.php` wird ueber `location.pathname` erkannt, nicht ueber
|
||||
`here`.** Der Login-Redirect haengt "index.php" gelegentlich als Wert eines
|
||||
@@ -203,9 +235,144 @@ Original-Zurueck/Weiter-Links, das Sheet verschwindet.
|
||||
einer verschachtelten Tabelle, `closest('tr')` trifft die innere Zeile ohne
|
||||
Autor- und Profilspalte.
|
||||
|
||||
**`DEBUG` steht auf `true`** und muss vor dem Ausliefern auf `false`. Die
|
||||
Versionsnummer steht doppelt: im `@version`-Header und im Text des
|
||||
Debug-Overlays.
|
||||
**Der Umbau erbt die Scrollposition, die der Browser vorher gesetzt hat.**
|
||||
`replaceBody()` laeuft auf `DOMContentLoaded` und ruehrt `scrollY` nicht an;
|
||||
weil ein Thread auch danach laenger als der Viewport bleibt, klemmt der Browser
|
||||
sie nie auf 0 zurueck. Auf dem iPhone startete der Thread deshalb mitten im
|
||||
ersten Beitrag: iOS Safari stellt die Position einer schon besuchten Adresse
|
||||
von sich aus wieder her und verankert sie ausserdem beim Relayout auf
|
||||
`width=device-width`. Im Chrome-Debugging faellt das nicht auf, weil dort jede
|
||||
Adresse frisch geladen wird und es gar keine Position zu uebernehmen gibt. Zwei
|
||||
Bremsen dagegen: `history.scrollRestoration = 'manual'` direkt beim
|
||||
Script-Start — spaeter waere es zu spaet, Safari hat dann laengst gescrollt —
|
||||
und `holdStart()` am Ende von `run()`.
|
||||
|
||||
Ein einzelner Reset auf `DOMContentLoaded` reichte nicht — am Geraet
|
||||
widerlegt: Safari legt die Position nicht in einem Rutsch fest, sondern
|
||||
springt nach `load` und nach dem Layout nachgeladener Bilder noch einmal nach
|
||||
und ueberschreibt den Reset. `holdStart()` haelt den Anfang deshalb fest —
|
||||
zwoelf Versuche im 50-ms-Takt plus einer auf `load` fuer den Sprung, der erst
|
||||
nach dem letzten Bild kommt. Die Bremse loest bei der ersten eigenen Geste des
|
||||
Lesers aus (`touchstart`/`wheel`/`keydown`, jeweils `once`), sonst risse sie
|
||||
ihn mitten im Lesen zurueck. `scroll` taugt als Signal dafuer nicht:
|
||||
`toStart()` loest selbst eines aus und wuerde sich sofort selbst abschalten.
|
||||
|
||||
**Die Post-Anker der Originalseite muessen auf die Karten wandern.** Ein
|
||||
Beitrag traegt sein Sprungziel als `<a name="pXXXX">` in der Autorenzelle, und
|
||||
die verschwindet mit dem Original-DOM. Bis 3.14 hatte `#pXXXX` deshalb kein
|
||||
Ziel mehr — der „Neueste ↓"-Link aus der Themenliste landete nicht am Beitrag,
|
||||
sondern dort, wo der Browser beim Umbau zufaellig stand. `buildThread()`
|
||||
uebernimmt den Namen jetzt als `id` der `.rm-post`-Karte, und `toStart()`
|
||||
springt darauf statt an den Anfang, wenn die Adresse ein Fragment traegt.
|
||||
`scroll-margin-top` auf `.rm-post` haelt dabei die fixierte Brotkrumenleiste
|
||||
frei, sonst steht der Kartenkopf dahinter.
|
||||
|
||||
**Die Lupe und die Login-Pille haengen an einer gemeinsamen `margin-left:auto`
|
||||
auf einem Wrapper (`.rm-auth-group`), nicht je an sich selbst.** Saesse die
|
||||
Regel wie zuvor direkt auf `.rm-auth`, wuerde nur die Pille an den Rand
|
||||
geschoben und die Lupe bliebe links davon kleben — mit sichtbarer Luecke
|
||||
dazwischen statt „direkt links neben der Pille". Die Lupe existiert im Markup
|
||||
ohnehin nur bei bestehender Sitzung; ausgeloggt bleibt der Wrapper mit der
|
||||
Pille als einzigem Kind und verhaelt sich exakt wie vorher.
|
||||
|
||||
**`#topic-search` steht dreifach identisch im Thread-Markup** (Kopf, Fuss,
|
||||
ueber der Beitragsliste), jedes Mal mit derselben, eigentlich unzulaessigen
|
||||
`id`. `document.querySelector('#topic-search')` trifft einfach das erste und
|
||||
verschiebt es unveraendert in die zweite Leiste — alle drei tragen dieselben
|
||||
versteckten Felder (`t`, `sf`, `terms`), welches der drei benutzt wird, ist
|
||||
gleichgueltig.
|
||||
|
||||
**`select[name="fid[]"]` ist der einzige verlaessliche Marker fuer das grosse
|
||||
Suchformular auf `search.php`.** Sowohl `viewforum.php` als auch
|
||||
`viewtopic.php` betten ein eigenes Mini-Suchformular ein (`#forum-search` /
|
||||
`#topic-search`, „Search this forum…" / „Search this topic…"), beide mit
|
||||
`action="./search.php"` und einem Feld `name="keywords"` — nach diesen Merkmalen
|
||||
allein liesse sich das grosse Formular nicht von den kleinen unterscheiden.
|
||||
Der mehrfach waehlbare Forenwaehler existiert dagegen nur im grossen Formular.
|
||||
|
||||
**Die Vorbelegung des Forenwaehlers haengt an der harten Forum-ID 10.**
|
||||
`buildSearchForm()` waehlt `option[value="10"]` (Main Forum) vor und setzt
|
||||
`input[name="sc"][value="0"]` (Unterforen: No), weil das serverseitige
|
||||
Formular sonst mit leerem Waehler und „Search subforums: Yes" startet. Beides
|
||||
ist eine Bequemlichkeit, keine Funktion: vergibt das Forum die IDs neu oder
|
||||
benennt es das Hauptforum um, greift die Vorauswahl stumm nicht mehr — das
|
||||
Formular bleibt bedienbar, steht dann aber wieder auf der Servervorgabe. Kein
|
||||
Fehlerfall, aber auch nichts, was von selbst auffaellt.
|
||||
|
||||
**Die Textfelder des Suchformulars tragen `style="width: 300px"` direkt im
|
||||
Markup.** Eine externe Regel ohne `!important` sticht dagegen nicht, egal wie
|
||||
spezifisch der Selektor ist — `buildSearchForm()` entfernt das
|
||||
`style`-Attribut deshalb, statt gegen es anzuschreiben.
|
||||
|
||||
**Eine abgeschickte Suche liefert keine Themenliste, sondern einen ganz
|
||||
anderen Aufbau.** Die anfaengliche Annahme, Trefferzeilen tragen dieselben
|
||||
`topictitle`-Links wie eine Themenliste und laufen unveraendert durch
|
||||
`buildList()`, stimmte nicht — zumindest nicht im Modus "Posts" (`sr=posts`),
|
||||
der ueber die zweite Leiste im Thread entsteht, weil deren Formular kein `sr`
|
||||
setzt. Jeder Treffer ist stattdessen ein Beitragsauszug: Autor, Zeitstempel,
|
||||
Forum/Thema-Kontext, ein Stueck `postbody`. `buildSearchResults()` erkennt
|
||||
diesen Modus an `p.topictitle` — als `<p>`, nicht als `<a>` wie das
|
||||
gleichnamige Klassenpendant einer Themenzeile in `viewforum.php` — und baut
|
||||
daraus eigene Karten (`buildHit()`), bevor `buildList()`s allgemeines
|
||||
Zeileneinsammeln ueberhaupt anfaengt und die enthaltenen `viewtopic.php`-Links
|
||||
falsch klassifizieren wuerde. Ungeprueft bleibt der Modus "Topics" (`sr=topics`,
|
||||
Vorauswahl im grossen Suchformular) — dafuer liegt keine Fixture vor, dort
|
||||
sehen Treffer laut phpBB-Doku tatsaechlich wie topictitle-Zeilen aus und
|
||||
wuerden vermutlich durch den regulaeren Listenzweig laufen, das ist aber nicht
|
||||
verifiziert.
|
||||
|
||||
**Ein Treffer im Modus "Posts" besteht aus drei `<tr>`, unterscheidbar nur an
|
||||
der Position, nicht an einer Klasse.** Titelzeile (`p.topictitle`), Autor-
|
||||
und Betreffzeile, dann Antworten/Views samt Beitragsauszug — alle drei
|
||||
tragen row1/row2 wahllos, die einzige verlaessliche Zuordnung ist
|
||||
`nextElementSibling` von der Titelzeile aus.
|
||||
|
||||
**Der Beitragsauszug haengt in `<td class="postbody">`, nicht in
|
||||
`div`/`span.postbody` wie im echten Thread.** `buildHit()` verschiebt das
|
||||
`<td>` trotzdem unveraendert per `appendChild()` in `.rm-body` — das ist reine
|
||||
DOM-Manipulation, keine Neuparse, darum meckert der Browser nicht ueber ein
|
||||
`<td>` ausserhalb einer Tabelle. Es rendert danach aber weiterhin als
|
||||
`display:table-cell`, wenn `.rm-root.rm-hits .rm-body > td` das nicht
|
||||
ausdruecklich auf `block` zwingt.
|
||||
|
||||
**"Replies: 5403" und "Views: 951837" stehen im Markup nur durch ein `<br />`
|
||||
getrennt, ohne Leerzeichen.** `txt()` normalisiert Whitespace, erzeugt aber
|
||||
keinen wo im Original-Markup keiner steht — `txt(meta)` liefert deshalb
|
||||
`5403Views:951837` statt zwei getrennter Werte. `buildHit()` greift beide
|
||||
Zahlen einzeln per Regex heraus und reicht sie an dieselbe `metaRow()` wie bei
|
||||
Themenkarten weiter, die den Trenner per CSS (`::before`) selbst einfuegt.
|
||||
|
||||
**`input[type="submit"]`/`input[type="reset"]` setzen eigenes Padding und
|
||||
Zentrierung in iOS Safari unzuverlaessig um — auch mit `-webkit-appearance:
|
||||
none`.** Zuerst aufgefallen bei Suchen/Zuruecksetzen im Suchformular
|
||||
nebeneinander, wo die feste Flex-Aufteilung die Verzerrung besonders sichtbar
|
||||
machte: zu schmale Buttons, Text nicht mittig. `-webkit-appearance:none` plus
|
||||
`box-sizing:border-box`/`line-height:normal` (erster Versuch) reichte laut
|
||||
Test am Geraet nicht aus — das legt einen tieferen WebKit-Bug nahe, bei dem
|
||||
`input[type=submit/reset]` die native Knopfoptik trotz `appearance:none` nicht
|
||||
vollstaendig ablegt. Der tragfaehige Fix ist `inputToButton()`: die
|
||||
Original-`<input>`-Elemente werden vor dem Verschieben durch echte
|
||||
`<button type="submit/reset">` ersetzt (Name und Value wandern mit, am
|
||||
uebermittelten Formularfeld aendert sich nichts) — `<button>` wird von WebKit
|
||||
unter `appearance:none` zuverlaessig respektiert. Betrifft grundsaetzlich
|
||||
jeden Submit-Button im Script: Suchformular, Login, Thread-Suchleiste nutzen
|
||||
alle `inputToButton()`. Ungeprueft, ob `-webkit-appearance:none` allein bei
|
||||
einer anderen iOS-Version genuegt haette — der Wechsel auf `<button>` umgeht
|
||||
die Frage, statt sie zu beantworten.
|
||||
|
||||
**`DEBUG` muss vor dem Ausliefern auf `false` stehen** (aktuell tut es das).
|
||||
Solange es auf `true` steht, haengt `run()` das gruene Overlay an — das ist
|
||||
der einzige Effekt des Flags, ein vergessenes `true` bricht nichts, sieht auf
|
||||
dem Geraet aber unfertig aus.
|
||||
|
||||
**Die Versionsnummer steht nur im `@version`-Header.** Das Overlay liest sie
|
||||
ueber `GM_info.script.version` zurueck, statt sie ein zweites Mal als Literal
|
||||
zu fuehren — genau das lief zwischen 3.13 und 3.14 auseinander, ohne dass es
|
||||
jemandem auffiel, weil das Overlay im Auslieferzustand ohnehin aus ist. Wer
|
||||
den Overlay-Text anfasst: keinen neuen Versionsliteral einbauen. Fehlt
|
||||
`GM_info` (Manager ohne diese API), zeigt das Overlay den Namen ohne Version;
|
||||
das ist der gewollte Rueckfall, weil eine fehlende Angabe harmloser ist als
|
||||
eine falsche.
|
||||
|
||||
## Testen
|
||||
|
||||
@@ -229,6 +396,22 @@ Drei Stolpersteine dabei:
|
||||
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`.
|
||||
Am Beitrag selbst zu pruefen: alles hinter dem ersten `<hr>` im `postbody`
|
||||
landet als zugeklapptes `details` („Signatur") am Kartenfuss, Beitraege ohne
|
||||
`<hr>` behalten ihren Text vollstaendig im Koerper.
|
||||
|
||||
Fuer den Sprung an den Threadanfang genuegt derselbe Fixture zweimal, einmal
|
||||
mit und einmal ohne Fragment in der Adresse: ohne Fragment darf nur
|
||||
`scrollTo(0, 0)` fallen, mit `#pXXXX` nur ein `scrollIntoView()` auf genau der
|
||||
Karte mit dieser `id` — und jede der 15 Karten muss eine tragen. Fuer
|
||||
`holdStart()` kommen drei zeitabhaengige Faelle dazu: dass ohne Zutun weitere
|
||||
Resets nachkommen, dass ein abgesetztes `touchstart` sie sofort stoppt, und
|
||||
dass `load` nach dem Ende der Schleife noch einmal greift, aber nach einer
|
||||
eigenen Geste eben nicht mehr. Achtung: jsdom
|
||||
kennt `history.scrollRestoration` nicht, das Script laeuft dort stumm in seinen
|
||||
Rueckfall. Wer den Zweig pruefen will, definiert die Eigenschaft vor `eval()`
|
||||
selbst auf `window.history`. `scrollTo`, `scrollIntoView` und
|
||||
`requestAnimationFrame` fehlen ohnehin und muessen gestubbt werden.
|
||||
|
||||
Dieselben Faelle gelten seit 3.5 fuer die Liste, die sich die Leiste mit dem
|
||||
Thread teilt. Eckdaten von `dev/viewforum.php`: `f=10`, `per_page = 100`, 610
|
||||
@@ -251,22 +434,44 @@ dieselbe Forenliste ohne die Klassen `forumlink`/`forumdesc` — die muss ueber
|
||||
die Zahlenspalten dasselbe Ergebnis liefern.
|
||||
|
||||
Fuer den Login genuegt `dev/ucp.php` unveraendert: geprueft werden das
|
||||
uebernommene Formular (Username-/Passwortfeld, `sid`, `redirect`, Submit) und
|
||||
die Pille im ausgeloggten Zustand. Fuer den eingeloggten Zustand der Pille
|
||||
`dev/index.php` oder `dev/after_login/ucp.php` laden — beide sind eingeloggt
|
||||
uebernommene Formular (Username-/Passwortfeld, `sid`, `redirect`, Submit), der
|
||||
Regeltext als zugeklapptes `details` („Regeln") und die Pille im ausgeloggten
|
||||
Zustand. Fuer den eingeloggten Zustand der Pille `dev/index.php`,
|
||||
`dev/search.php` oder `dev/search2.php` laden — alle drei sind eingeloggt
|
||||
aufgenommen und tragen das echte `Logout [ Name ]`-Markup.
|
||||
|
||||
Fuer die Meldung nach dem Login genuegt `dev/after_login/ucp.php`
|
||||
unveraendert, URL `ucp.php?mode=login`: geprueft werden Titel, uebernommene
|
||||
Nachricht samt Link und die Pille. Zu pruefen ist ausserdem, dass `dev/ucp.php`
|
||||
(das Formular, gleiche URL) weiterhin die Formular-Zweig traf und nicht in die
|
||||
Meldungs-Erkennung faellt.
|
||||
Die Meldung nach dem Login (`buildLoginInfo()`) ist mangels Fixture nicht
|
||||
nachpruefbar. Wer sie anfasst, nimmt `ucp.php?mode=login` nach einem echten
|
||||
Login neu auf; zu pruefen waeren dann Titel, uebernommene Nachricht samt Link
|
||||
und die Pille — und dass `dev/ucp.php` (das Formular, gleiche URL) weiterhin
|
||||
im Formular-Zweig landet und nicht in die Meldungs-Erkennung faellt.
|
||||
|
||||
Fuer die Forumwurzel genuegt `dev/index.php`, URL `index.php?sid=…`: zwei
|
||||
Kategorien mit zusammen 16 Forenkarten, keine Paginierung. Zu pruefen ist
|
||||
zusaetzlich eine `ucp.php?mode=login&redirect=index.php`-URL — sie muss trotz
|
||||
des `index.php`-Substrings im Login-Zweig landen, nicht im Listen-Zweig.
|
||||
|
||||
Fuer das Suchformular genuegt `dev/search.php` unveraendert, URL
|
||||
`search.php`: geprueft werden der Forenwaehler als Erkennungsmerkmal, das
|
||||
verschobene Formular samt versteckter Felder (`t`, `sid`), die entfernten
|
||||
inline-`style`-Attribute der Textfelder und die Schnellzugriffs-Chips aus
|
||||
`p.searchbar`. Zu pruefen ist ausserdem `dev/viewforum.php` auf derselben
|
||||
Weiche: dessen eingebettetes `#forum-search`-Mini-Formular teilt sich Aktion
|
||||
und `keywords`-Feld mit dem grossen Suchformular und darf `buildSearchForm()`
|
||||
nicht faelschlich ausloesen, die Forenliste muss wie gewohnt Karten liefern.
|
||||
|
||||
Fuer Suchtreffer genuegt `dev/search2.php` unveraendert, URL beliebig (die
|
||||
Erkennung haengt nicht an `location`, sondern an `p.topictitle` im Markup):
|
||||
geprueft werden alle 15 Trefferkarten, die verschobene "Searched topic /
|
||||
Search term used"-Zelle samt ihrer beiden Links, Autor und Zeitstempel pro
|
||||
Treffer, die per Regex getrennten Antworten-/Views-Zahlen, der verschobene
|
||||
`td.postbody`-Beitragsauszug, der „Zum Beitrag ↓"-Link am Kartenfuss und die
|
||||
Seitennavigation (`per_page=15`, 96 Seiten). Zu pruefen ist ausserdem, dass `dev/search.php` (das leere Formular)
|
||||
weiterhin den Formular-Zweig trifft und nicht in die Treffer-Erkennung faellt,
|
||||
und dass `dev/viewforum.php` weiterhin normale Karten liefert — beide haben
|
||||
kein `p.topictitle`. Der Modus "Topics" (`sr=topics`) ist mangels Fixture
|
||||
ungeprueft.
|
||||
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# iOS Safari Userscripts — cs.rin.ru
|
||||
# RIN Mobile — cs.rin.ru for iOS Safari
|
||||
|
||||
[English](#english) | [Deutsch](#deutsch)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## English
|
||||
|
||||
A single userscript that rebuilds the phpBB2 pages of `cs.rin.ru` into a mobile
|
||||
A single userscript that rebuilds the phpBB3 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>`.
|
||||
|
||||
@@ -15,82 +15,57 @@ page type:
|
||||
|
||||
| Page | View |
|
||||
|---|---|
|
||||
| `viewforum.php`, `search.php`, `index.php`, forum root | Topic and forum list as cards |
|
||||
| `viewforum.php`, `index.php`, forum root | Topic and forum list as cards |
|
||||
| `search.php` | The query form restyled, or — for a submitted search in "Posts" mode — its hits as cards |
|
||||
| `viewtopic.php` | Thread as a post stream with a fixed bottom navigation bar |
|
||||
| `ucp.php?mode=login` | Login form, restyled in place; on the same URL after a successful submit, the resulting information page instead |
|
||||
| anything else | Untouched — no styles are injected |
|
||||
|
||||
A `viewforum.php` page is not necessarily a list of topics: a category page
|
||||
lists forums instead, and a forum page can carry both. Each table row is
|
||||
therefore classified by its first usable link. Forum rows become cards with
|
||||
name, description, subforum links and counts; topic rows keep their existing
|
||||
card; category headers become section headings. Rows are recognised by phpBB's
|
||||
own `forumlink` / `forumdesc` classes, and failing that by their shape — two
|
||||
purely numeric columns are only ever a forum's topic and post count.
|
||||
### Screenshots
|
||||
|
||||
Forums, global announcements, announcements, stickies and topics all end up in
|
||||
the same stream of cards, which loses the separation the original page draws
|
||||
between them. A header strip naming the section and its card count is therefore
|
||||
inserted wherever the kind changes, and forum cards additionally carry a
|
||||
coloured left edge. The label is taken from the board's own separator row
|
||||
(`Global Announcements`, `Stickies`, `Topics`), so sections this script does not
|
||||
know are named correctly too. Which kind a topic row is comes from the icon in
|
||||
its first column, not from its title. The strip is left out directly below a
|
||||
category heading, which already introduces the forums under it.
|
||||
| Forum list, with the sections above the topics collapsed | Thread, with the in-topic search bar revealed |
|
||||
|---|---|
|
||||
| <img src="docs/Screenshot_mobile_1.png" alt="Forum list as cards: breadcrumb bar with magnifier, an open FOREN section with four forum cards, collapsed GLOBAL ANNOUNCEMENTS, ANNOUNCEMENTS and STICKIES strips, and the page bar showing 1 / 610" width="100%"> | <img src="docs/Screenshot_mobile_2.png" alt="Thread view: breadcrumbs, a second bar with a search field, and a post card with avatar, author, rank and post count above the message" width="100%"> |
|
||||
| **Search form, top: quick links and forum picker** | **Search form, bottom: options and buttons** |
|
||||
| <img src="docs/Screenshot_mobile_3.png" alt="Search form: quick links as chips, the keyword fields and the forum picker with Main Forum preselected" width="100%"> | <img src="docs/Screenshot_mobile_4.png" alt="Search form continued: search subforums set to No, the remaining option groups, and the Search and Reset buttons" width="100%"> |
|
||||
|
||||
Tapping a header collapses its section. Global announcements, announcements and
|
||||
stickies start collapsed: they precede the topics on *every* page of a forum and
|
||||
otherwise fill the first screen. The state is stored per kind in `localStorage`,
|
||||
so collapsing stickies once applies to every forum. `localStorage` is used
|
||||
rather than `GM_setValue` because it needs no `@grant` and behaves the same in
|
||||
Tampermonkey and in the iOS app; the trade-off is that Safari may clear
|
||||
script-writable storage after long inactivity, which resets the sections to
|
||||
their default.
|
||||
### What it does
|
||||
|
||||
Both views share the same fixed navigation bar at the bottom: first / previous /
|
||||
next / last and a page indicator. Tapping the indicator opens a sheet with a
|
||||
number field and a grid of all pages. Targets are computed as
|
||||
`start = (page - 1) × per_page` rather than taken from the rendered page links,
|
||||
because phpBB only ever shows a handful of 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. In the list the bar is
|
||||
built only where there is more than one page — forum pages run to hundreds of
|
||||
them, category pages to one.
|
||||
The original page is hidden and a mobile layout is built from the same data,
|
||||
one view per page type:
|
||||
|
||||
Both views carry a fixed breadcrumb bar at the top, taken from the forum's own
|
||||
`Board index » … » Main Forum` trail. Without it there is no way back out of a
|
||||
thread, because the original page is hidden and phpBB links the parent forum
|
||||
nowhere else. The bar scrolls horizontally if the trail is too long for the
|
||||
screen, and is left out entirely on pages that do not carry a trail.
|
||||
- **Lists as cards.** Forum and topic rows become cards with title, description,
|
||||
counts and last post; category headers become section headings.
|
||||
- **Section strips.** Forums, announcements, stickies and topics are separated
|
||||
by a header carrying the section name and its card count. Announcements and
|
||||
stickies start collapsed — they repeat above the topics on *every* page of a
|
||||
forum. Tapping a header toggles its section, and the state is remembered per
|
||||
section across forums.
|
||||
- **Fixed page bar.** First / previous / next / last and a page indicator.
|
||||
Tapping the indicator opens a sheet with a number field and every page of the
|
||||
forum or thread, not just the handful phpBB renders.
|
||||
- **Fixed breadcrumb bar** with a login pill showing the account name. Without
|
||||
it there is no way back out of a thread, since the original page is hidden.
|
||||
- **Search from anywhere.** With a session, a magnifier sits next to the pill:
|
||||
it opens `search.php`, or inside a thread reveals a bar that searches the
|
||||
topic.
|
||||
- **Readable posts.** Avatar, author, rank and timestamp above the message;
|
||||
signatures fold into a collapsed box.
|
||||
- **Forms are moved, not rebuilt.** Login, search and the page after a
|
||||
successful login reuse the original elements, so hidden fields and the plain
|
||||
server-side POST stay intact. The search form starts on Main Forum with
|
||||
subforums off, and the board rules on the login page fold away.
|
||||
- **Search hits as cards** in "Posts" display mode, each with a link straight to
|
||||
the post. "Topics" mode falls through to the regular list view.
|
||||
|
||||
The breadcrumb bar also carries a login pill, built on every page the script
|
||||
touches, not only the login form itself. Session state is read from the
|
||||
forum's own menu bar: a `mode=login` link means logged out, a `mode=logout`
|
||||
link means logged in, and the pill switches between a "Login" link and the
|
||||
account name accordingly. The name is parsed from the link text with a
|
||||
`Logout [ Name ]` pattern, confirmed against a real logged-in session's markup;
|
||||
if it ever fails to match — a different language, a different bracket style —
|
||||
the pill still shows "Logout", just without a name, rather than breaking.
|
||||
The script's own labels are German; anything taken from the board keeps its
|
||||
original wording — `FOREN` in the first screenshot is the script's name for a
|
||||
group the board does not label, while `STICKIES` and `TOPICS` are the board's
|
||||
own words.
|
||||
|
||||
The login form itself is not rebuilt from scratch: the original `<form>`
|
||||
element is moved into the new layout and restyled, table rows collapsed to a
|
||||
vertical stack. Reusing the element rather than reconstructing it keeps the
|
||||
hidden `sid` and `redirect` fields, the `autologin` / `viewonline` checkboxes
|
||||
and the plain server-side POST intact — the script never talks to the forum's
|
||||
session handling directly, it only changes how the existing form looks.
|
||||
|
||||
Submitting the form re-renders the same URL with a generic "Information"
|
||||
template instead of the form — a short confirmation ("You have been
|
||||
successfully logged in.") before phpBB's own `<meta refresh>` moves on to the
|
||||
forum index. The script recognises this template by its shape (a single-cell
|
||||
header followed by a message cell), not by its wording, and restyles it the
|
||||
same way as the form: the original cell — message and link included — is
|
||||
reused in place rather than rebuilt.
|
||||
|
||||
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.
|
||||
It runs at `document-start` and injects a mobile viewport meta tag. A `DEBUG`
|
||||
flag in the source, off by default, adds a green diagnostic overlay showing the
|
||||
version, the chosen view and how many rows were converted.
|
||||
|
||||
### Installation on iOS
|
||||
|
||||
@@ -114,7 +89,7 @@ Tampermonkey.
|
||||
The loader in `dev/` references the script by **absolute path**:
|
||||
|
||||
```js
|
||||
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.js
|
||||
// @require file:///C:/Projekte/rin-mobile-userscript/rin-mobile.user.js
|
||||
```
|
||||
|
||||
This path is hardcoded and only valid on the machine it was written for. After
|
||||
@@ -122,7 +97,7 @@ cloning the repository or moving the project folder, adjust the `@require` line
|
||||
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`
|
||||
`file:///D:/projects/rin-mobile-userscript/rin-mobile.user.js`. A `@require`
|
||||
that cannot be resolved causes Tampermonkey to skip the script **entirely and
|
||||
without an error message on the page**, which looks exactly like the script not
|
||||
being installed at all.
|
||||
@@ -150,26 +125,28 @@ cards and two debug overlays.
|
||||
|
||||
#### Test fixtures
|
||||
|
||||
`cs.rin.ru` requires a login, so save the pages you need once and iterate
|
||||
against local copies:
|
||||
Layout work happens on the live site, with the loader active in Chrome. The
|
||||
saved pages under `dev/` serve a different purpose: `cs.rin.ru` requires a
|
||||
login, and its markup is the only source for what the script has to cope with,
|
||||
so the pages that matter are kept as snapshots — as a reference to read, and as
|
||||
input for a throwaway jsdom harness when a change needs to be checked against
|
||||
several pages at once.
|
||||
|
||||
1. Open the page while logged in, then in DevTools right-click `<html>` →
|
||||
*Copy outerHTML*.
|
||||
2. Save it as `dev/viewtopic.php.html` or `dev/viewforum.php.html`.
|
||||
3. Serve the folder: `python -m http.server 8777 --directory .`
|
||||
2. Save it under the page's own name — `dev/viewtopic.php`, `dev/viewforum.php`,
|
||||
`dev/index.php`, `dev/ucp.php`, `dev/search.php`.
|
||||
|
||||
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 loader's `@include` rules
|
||||
and the page-type check in `rin-mobile.user.js` match against.
|
||||
The loader deliberately matches `cs.rin.ru` only, so these files are not meant
|
||||
to be opened in the browser. Serving them locally would take `@include` rules
|
||||
for `localhost`, and those match on page names common enough (`index.php`,
|
||||
anything containing `search`) that the script would inject its stylesheet into
|
||||
unrelated local projects.
|
||||
|
||||
Forum images and stylesheets are missing from such a snapshot. That is fine for
|
||||
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`
|
||||
placeholders instead of links, and may use different markup for author names
|
||||
than the logged-in view.
|
||||
A snapshot saved while logged out contains `span.link_removed` placeholders
|
||||
instead of links, and may use different markup for author names than the
|
||||
logged-in view — worth knowing before puzzling over a fixture that behaves
|
||||
differently from the real page.
|
||||
|
||||
#### Device emulation
|
||||
|
||||
@@ -195,32 +172,35 @@ Windows (`npx playwright install webkit`).
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
Install `dev/tm-test.user.js` — it injects a red bar and nothing else. If the
|
||||
bar does not appear, the problem is Tampermonkey (see setup above), not the
|
||||
script code. If it does appear, check the loader's *Externals* tab in the
|
||||
Tampermonkey dashboard: an empty or failed entry means the `@require` path is
|
||||
wrong.
|
||||
If nothing happens at all, separate the two possible causes before reading any
|
||||
code. Check the loader's *Externals* tab in the Tampermonkey dashboard: an
|
||||
empty or failed entry means the `@require` path is wrong. If the entry is
|
||||
there and the script still does nothing, set `DEBUG = true` in
|
||||
`rin-mobile.user.js` — the green overlay appears on every page the script
|
||||
touches, so its absence means the script never ran, while an overlay reporting
|
||||
`ansicht=keine` means it ran but the URL matched no view.
|
||||
|
||||
If Tampermonkey keeps serving a stale copy despite the update interval, replace
|
||||
the `file://` require with a local HTTP server, which does not cache:
|
||||
|
||||
```js
|
||||
// @require http://localhost:8777/rin-mobile.user.js
|
||||
```
|
||||
If Tampermonkey keeps serving a stale copy despite the update interval, open
|
||||
the loader's *Externals* tab and trigger an update on the `@require` entry by
|
||||
hand, or disable and re-enable the loader — both discard the cached copy.
|
||||
|
||||
### Repository layout
|
||||
|
||||
```
|
||||
rin-mobile.user.js topic list → cards, thread → post stream
|
||||
rin-mobile.user.js all views: lists, thread, login, search
|
||||
CLAUDE.md notes for working on the script
|
||||
dev/
|
||||
rin-mobile.dev.user.js loader, @require with absolute path
|
||||
*.php saved pages, markup reference, not in git
|
||||
docs/
|
||||
Screenshot_mobile_*.png the screenshots above
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deutsch
|
||||
|
||||
Ein Userscript, das die phpBB2-Seiten von `cs.rin.ru` für Safari auf iOS in eine
|
||||
Ein Userscript, das die phpBB3-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.
|
||||
@@ -230,89 +210,61 @@ anhand des Seitentyps:
|
||||
|
||||
| Seite | Ansicht |
|
||||
|---|---|
|
||||
| `viewforum.php`, `search.php`, `index.php`, Forumwurzel | Themen- und Forenliste als Karten |
|
||||
| `viewforum.php`, `index.php`, Forumwurzel | Themen- und Forenliste als Karten |
|
||||
| `search.php` | Das Suchformular umgestylt oder — bei abgeschickter Suche im Modus „Posts" — die Treffer als Karten |
|
||||
| `viewtopic.php` | Thread als Beitragsstrom mit fixierter Navigationsleiste |
|
||||
| `ucp.php?mode=login` | Login-Formular, an Ort und Stelle umgestylt; auf derselben URL nach erfolgreichem Absenden stattdessen die Informationsseite |
|
||||
| alles andere | Unverändert — es werden keine Stile injiziert |
|
||||
|
||||
Eine `viewforum.php`-Seite ist nicht zwangsläufig eine Themenliste: eine
|
||||
Kategorieseite listet Foren auf, eine Forenseite kann beides enthalten. Jede
|
||||
Tabellenzeile wird deshalb anhand ihres ersten verwertbaren Links eingeordnet.
|
||||
Forenzeilen werden zu Karten mit Name, Beschreibung, Unterforen und Zahlen,
|
||||
Themenzeilen behalten ihre bisherige Karte, Kategoriezeilen werden zu
|
||||
Zwischenüberschriften. Erkannt werden die Zeilen an phpBBs eigenen Klassen
|
||||
`forumlink` / `forumdesc` und ersatzweise an ihrer Form — zwei rein numerische
|
||||
Spalten hat nur eine Forenzeile mit Themen- und Beitragszahl.
|
||||
### Screenshots
|
||||
|
||||
Foren, globale Ankündigungen, Ankündigungen, Stickies und Themen landen alle im
|
||||
selben Kartenstrom, womit die Trennung verlorengeht, die die Originalseite
|
||||
zwischen ihnen zieht. Bei jedem Wechsel wird deshalb ein Gruppenkopf mit dem
|
||||
Namen des Abschnitts und der Zahl seiner Karten eingezogen; Forenkarten tragen
|
||||
zusätzlich eine farbige linke Kante. Die Beschriftung stammt aus der Trennzeile
|
||||
der Seite selbst (`Global Announcements`, `Stickies`, `Topics`), damit auch
|
||||
Abschnitte richtig heißen, die dieses Script nicht kennt. Welcher Art eine
|
||||
Themenzeile ist, verrät das Symbol ihrer ersten Spalte, nicht der Titel. Direkt
|
||||
unter einer Kategorieüberschrift entfällt der Gruppenkopf, weil die Überschrift
|
||||
die Foren darunter schon einleitet.
|
||||
| Forenliste, die Abschnitte über den Themen zugeklappt | Thread, mit eingeblendeter Suchleiste |
|
||||
|---|---|
|
||||
| <img src="docs/Screenshot_mobile_1.png" alt="Forenliste als Karten: Brotkrumenleiste mit Lupe, aufgeklappter Abschnitt FOREN mit vier Forenkarten, zugeklappte Streifen für GLOBAL ANNOUNCEMENTS, ANNOUNCEMENTS und STICKIES, unten die Seitenleiste mit 1 / 610" width="100%"> | <img src="docs/Screenshot_mobile_2.png" alt="Thread-Ansicht: Brotkrumen, zweite Leiste mit Suchfeld, darunter eine Beitragskarte mit Avatar, Autor, Rang und Beitragszahl über dem Text" width="100%"> |
|
||||
| **Suchformular, oben: Schnellzugriffe und Forenwähler** | **Suchformular, unten: Optionen und Schaltflächen** |
|
||||
| <img src="docs/Screenshot_mobile_3.png" alt="Suchformular: Schnellzugriffe als Chips, die Feldgruppen für Suchbegriffe und der Forenwähler mit vorausgewähltem Main Forum" width="100%"> | <img src="docs/Screenshot_mobile_4.png" alt="Suchformular, Fortsetzung: Unterforen auf No, die übrigen Optionsgruppen sowie die Schaltflächen Search und Reset" width="100%"> |
|
||||
|
||||
Ein Tipp auf den Kopf klappt seinen Abschnitt zu. Globale Ankündigungen,
|
||||
Ankündigungen und Stickies starten zugeklappt: sie stehen auf *jeder* Seite
|
||||
eines Forums vor den Themen und füllen sonst den ersten Bildschirm. Der Zustand
|
||||
liegt je Art in `localStorage`, einmal zugeklappte Stickies gelten also in jedem
|
||||
Forum. `localStorage` statt `GM_setValue`, weil es ohne `@grant` auskommt und
|
||||
sich in Tampermonkey wie in der iOS-App gleich verhält; dafür kann Safari
|
||||
schreibbaren Speicher nach längerer Nichtnutzung räumen, dann stehen die
|
||||
Abschnitte wieder auf ihrer Voreinstellung.
|
||||
### Was das Script liefert
|
||||
|
||||
Beide Ansichten teilen sich dieselbe fixierte Navigationsleiste am unteren
|
||||
Rand: erste / vorherige / nächste / letzte Seite und eine Positionsanzeige. Ein
|
||||
Tipp darauf öffnet ein Sheet mit Zahlenfeld und einem Raster aller Seiten. Die
|
||||
Ziele werden als `start = (Seite - 1) × per_page` berechnet und nicht aus den
|
||||
gerenderten Seitenlinks übernommen, weil phpBB davon immer nur eine Handvoll
|
||||
anzeigt. `per_page` stammt aus der `jumpto()`-Funktion, die das Forum in die
|
||||
Seite einbettet; fehlt sie, bleiben nur die erste Seite und die ursprünglichen
|
||||
Zurück-/Weiter-Links nutzbar. In der Liste entsteht die Leiste nur, wo es mehr
|
||||
als eine Seite gibt — Forenseiten haben hunderte, Kategorieseiten eine.
|
||||
Die Originalseite wird ausgeblendet und aus denselben Daten eine mobile Ansicht
|
||||
gebaut, je eine pro Seitentyp:
|
||||
|
||||
Beide Ansichten haben oben eine fixierte Brotkrumenleiste, übernommen aus dem
|
||||
`Board index » … » Main Forum`-Pfad des Forums. Ohne sie führt aus einem Thread
|
||||
kein Weg zurück, weil die Originalseite ausgeblendet ist und phpBB das
|
||||
Elternforum nirgends sonst verlinkt. Die Leiste lässt sich waagerecht schieben,
|
||||
wenn der Pfad breiter ist als der Bildschirm, und entfällt auf Seiten ohne
|
||||
Pfadangabe.
|
||||
- **Listen als Karten.** Foren- und Themenzeilen werden zu Karten mit Titel,
|
||||
Beschreibung, Zahlen und letztem Beitrag, Kategoriezeilen zu
|
||||
Zwischenüberschriften.
|
||||
- **Gruppenköpfe.** Foren, Ankündigungen, Stickies und Themen trennt jeweils ein
|
||||
Kopf mit dem Namen des Abschnitts und der Zahl seiner Karten. Ankündigungen
|
||||
und Stickies starten zugeklappt — sie stehen auf *jeder* Seite eines Forums
|
||||
über den Themen. Ein Tipp auf den Kopf klappt seinen Abschnitt um, der Zustand
|
||||
gilt je Abschnitt über alle Foren hinweg.
|
||||
- **Fixierte Seitenleiste.** Erste / vorherige / nächste / letzte Seite und eine
|
||||
Positionsanzeige. Ein Tipp darauf öffnet ein Sheet mit Zahlenfeld und allen
|
||||
Seiten des Forums oder Threads, nicht nur der Handvoll, die phpBB rendert.
|
||||
- **Fixierte Brotkrumenleiste** mit einer Login-Pille, die den Kontonamen zeigt.
|
||||
Ohne sie führt aus einem Thread kein Weg zurück, weil die Originalseite
|
||||
ausgeblendet ist.
|
||||
- **Suche von überall.** Besteht eine Sitzung, sitzt neben der Pille eine Lupe:
|
||||
sie öffnet `search.php` oder blendet innerhalb eines Threads eine Leiste ein,
|
||||
die das Thema durchsucht.
|
||||
- **Lesbare Beiträge.** Avatar, Autor, Rang und Zeitstempel über dem Text,
|
||||
Signaturen wandern in einen zugeklappten Kasten.
|
||||
- **Formulare werden verschoben, nicht nachgebaut.** Login, Suche und die Seite
|
||||
nach erfolgreichem Login übernehmen die Original-Elemente, damit versteckte
|
||||
Felder und der schlichte serverseitige POST erhalten bleiben. Das Suchformular
|
||||
startet auf Main Forum ohne Unterforen, die Forenregeln im Login klappen weg.
|
||||
- **Suchtreffer als Karten** im Anzeigemodus „Posts", jeder mit einem Link
|
||||
direkt zum Beitrag. Der Modus „Topics" läuft durch die gewöhnliche
|
||||
Listenansicht.
|
||||
|
||||
Die Brotkrumenleiste trägt außerdem eine Login-Pille, gebaut auf jeder Seite,
|
||||
die das Script anfasst — nicht nur auf dem Login-Formular selbst. Der
|
||||
Sitzungszustand wird aus dem Menübalken der Originalseite gelesen: ein
|
||||
`mode=login`-Link bedeutet ausgeloggt, ein `mode=logout`-Link eingeloggt, die
|
||||
Pille wechselt entsprechend zwischen einem „Login"-Link und dem Kontonamen.
|
||||
Der Name wird über das Muster `Logout [ Name ]` aus dem Linktext gelesen, das
|
||||
gegen das Markup einer echten eingeloggten Sitzung bestätigt ist; greift es
|
||||
einmal nicht — andere Sprache, andere Klammerform — bleibt die Pille bei
|
||||
„Logout" stehen, nur ohne Namen, statt zu reißen.
|
||||
Die Beschriftungen des Scripts sind deutsch, alles aus dem Forum Übernommene
|
||||
behält seinen Wortlaut — `FOREN` im ersten Screenshot ist die Bezeichnung des
|
||||
Scripts für eine Gruppe, die die Seite selbst nicht beschriftet, `STICKIES` und
|
||||
`TOPICS` sind ihre eigenen Worte.
|
||||
|
||||
Das Login-Formular selbst wird nicht neu gebaut: das Original-`<form>`-Element
|
||||
wandert unverändert in das neue Layout und wird umgestylt, Tabellenzeilen
|
||||
werden zu einer senkrechten Abfolge zusammengeklappt. Die Wiederverwendung
|
||||
statt Neubau erhält die versteckten Felder `sid` und `redirect`, die
|
||||
Checkboxen `autologin` / `viewonline` und den schlichten serverseitigen POST —
|
||||
das Script spricht nie selbst mit der Sitzungsverwaltung des Forums, es ändert
|
||||
nur, wie das vorhandene Formular aussieht.
|
||||
|
||||
Nach dem Absenden liefert dieselbe URL statt des Formulars eine allgemeine
|
||||
„Information"-Vorlage aus — eine kurze Bestätigung („You have been
|
||||
successfully logged in."), bevor phpBBs eigenes `<meta refresh>` zum
|
||||
Forenindex weiterleitet. Das Script erkennt diese Vorlage an ihrer Form, nicht
|
||||
am Wortlaut (eine Kopfzeile mit einer einzelnen Zelle, gefolgt von einer
|
||||
Nachrichtenzelle), und stylt sie wie das Formular um: die Original-Zelle —
|
||||
Nachricht und Link inklusive — wandert unverändert an ihren Platz statt neu
|
||||
gebaut zu werden.
|
||||
|
||||
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.
|
||||
Das Script läuft bei `document-start` und setzt ein mobiles Viewport-Meta-Tag.
|
||||
Ein `DEBUG`-Flag im Quelltext, standardmäßig aus, legt ein grünes
|
||||
Diagnose-Overlay über die Seite: Version, gewählte Ansicht und Zahl der
|
||||
umgewandelten Zeilen.
|
||||
|
||||
### Installation unter iOS
|
||||
|
||||
@@ -336,7 +288,7 @@ laden — keine Neuinstallation in Tampermonkey nötig.
|
||||
Der Loader in `dev/` verweist mit einem **absoluten Pfad** auf das Script:
|
||||
|
||||
```js
|
||||
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.js
|
||||
// @require file:///C:/Projekte/rin-mobile-userscript/rin-mobile.user.js
|
||||
```
|
||||
|
||||
Dieser Pfad ist fest eingetragen und gilt nur auf dem Rechner, für den er
|
||||
@@ -345,7 +297,7 @@ 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`.
|
||||
verwenden — `file:///D:/projekte/rin-mobile-userscript/rin-mobile.user.js`.
|
||||
Ein `@require`, das nicht aufgelöst werden kann, führt dazu, dass Tampermonkey
|
||||
das Script **komplett und ohne Fehlermeldung auf der Seite** überspringt. Das
|
||||
sieht exakt so aus, als wäre gar kein Script installiert.
|
||||
@@ -372,6 +324,31 @@ 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.
|
||||
|
||||
#### Testfixtures
|
||||
|
||||
Die Layoutarbeit findet auf der echten Seite statt, mit dem Loader aktiv in
|
||||
Chrome. Die gespeicherten Seiten unter `dev/` dienen etwas anderem: `cs.rin.ru`
|
||||
verlangt einen Login, und sein Markup ist die einzige Quelle dafür, womit das
|
||||
Script zurechtkommen muss. Die relevanten Seiten liegen deshalb als Mitschnitt
|
||||
vor — zum Nachlesen und als Eingabe für ein Wegwerf-Harness mit jsdom, wenn
|
||||
eine Änderung gegen mehrere Seiten auf einmal zu prüfen ist.
|
||||
|
||||
1. Die Seite eingeloggt öffnen, in den DevTools mit der rechten Maustaste auf
|
||||
`<html>` → *Copy outerHTML*.
|
||||
2. Unter dem Namen der Seite ablegen — `dev/viewtopic.php`,
|
||||
`dev/viewforum.php`, `dev/index.php`, `dev/ucp.php`, `dev/search.php`.
|
||||
|
||||
Der Loader greift bewusst nur auf `cs.rin.ru`, diese Dateien sind also nicht
|
||||
zum Öffnen im Browser gedacht. Sie lokal auszuliefern verlangte
|
||||
`@include`-Regeln für `localhost`, und die passen auf Seitennamen, die dort
|
||||
häufig genug vorkommen (`index.php`, alles mit „search" darin), dass das Script
|
||||
sein Stylesheet in fremde lokale Projekte injizieren würde.
|
||||
|
||||
Ein ausgeloggt gespeicherter Mitschnitt enthält statt der Links
|
||||
`span.link_removed`-Platzhalter und kann Autorennamen anders auszeichnen als
|
||||
die eingeloggte Ansicht — gut zu wissen, bevor man über eine Fixture rätselt,
|
||||
die sich anders verhält als die echte Seite.
|
||||
|
||||
#### Geräte-Emulation
|
||||
|
||||
DevTools → *Device Toolbar* (`Strg+Umschalt+M`) → *Edit…* → eigenes Gerät mit
|
||||
@@ -397,24 +374,27 @@ Windows mit (`npx playwright install webkit`).
|
||||
|
||||
#### Fehlersuche
|
||||
|
||||
`dev/tm-test.user.js` installieren — das Script blendet nur einen roten Balken
|
||||
ein, sonst nichts. Erscheint der Balken nicht, liegt das Problem bei
|
||||
Tampermonkey (siehe Einrichtung oben) und nicht am Script-Code. Erscheint er,
|
||||
im Tampermonkey-Dashboard den Tab *Externals* des Loaders prüfen: ein leerer
|
||||
oder fehlgeschlagener Eintrag bedeutet, dass der `@require`-Pfad falsch ist.
|
||||
Passiert überhaupt nichts, zuerst die beiden möglichen Ursachen trennen, bevor
|
||||
Code gelesen wird. Im Tampermonkey-Dashboard den Tab *Externals* des Loaders
|
||||
prüfen: ein leerer oder fehlgeschlagener Eintrag bedeutet, dass der
|
||||
`@require`-Pfad falsch ist. Ist der Eintrag da und trotzdem passiert nichts, in
|
||||
`rin-mobile.user.js` `DEBUG = true` setzen — das grüne Overlay erscheint auf
|
||||
jeder Seite, die das Script anfasst. Bleibt es aus, lief das Script gar nicht;
|
||||
meldet es `ansicht=keine`, lief es, aber die URL passte auf keine Ansicht.
|
||||
|
||||
Liefert Tampermonkey trotz Update-Intervall weiterhin einen veralteten Stand
|
||||
aus, das `file://`-Require durch einen lokalen HTTP-Server ersetzen, der nicht
|
||||
zwischenspeichert:
|
||||
|
||||
```js
|
||||
// @require http://localhost:8777/rin-mobile.user.js
|
||||
```
|
||||
aus, im Tab *Externals* des Loaders den `@require`-Eintrag von Hand
|
||||
aktualisieren oder den Loader einmal deaktivieren und wieder aktivieren —
|
||||
beides verwirft die zwischengespeicherte Fassung.
|
||||
|
||||
### Aufbau des Repositories
|
||||
|
||||
```
|
||||
rin-mobile.user.js Themenliste → Karten, Thread → Beitragsstrom
|
||||
rin-mobile.user.js alle Ansichten: Listen, Thread, Login, Suche
|
||||
CLAUDE.md Notizen zur Arbeit am Script
|
||||
dev/
|
||||
rin-mobile.dev.user.js Loader, @require mit absolutem Pfad
|
||||
*.php gespeicherte Seiten, Markup-Vorlage, nicht in git
|
||||
docs/
|
||||
Screenshot_mobile_*.png die Screenshots oben
|
||||
```
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
// @name [DEV] RIN Mobile
|
||||
// @namespace rin-dev
|
||||
// @description Dev-Loader: laedt rin-mobile.user.js live von der Platte
|
||||
// @version 2.0
|
||||
// @version 2.1
|
||||
// @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
|
||||
// @require file:///C:/Projekte/rin-mobile-userscript/rin-mobile.user.js
|
||||
// @run-at document-start
|
||||
// @grant none
|
||||
// ==/UserScript==
|
||||
|
||||
// Der eigentliche Code steckt in der @require-Datei.
|
||||
// Aenderungen dort greifen nach einem Reload der Seite.
|
||||
//
|
||||
// Der Loader laeuft nur auf der echten Seite. Die Fixtures unter dev/ sind
|
||||
// Markup-Vorlagen fuer das jsdom-Harness, keine Seiten zum Anschauen im
|
||||
// Browser — dafuer braeuchte es einen lokalen Server und passende
|
||||
// @include-Zeilen, die auf localhost zu breit streuen wuerden.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
+487
-7
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name RIN Mobile
|
||||
// @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom
|
||||
// @version 3.8
|
||||
// @version 3.15
|
||||
// @match *://cs.rin.ru/forum/*
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
@@ -12,11 +12,36 @@
|
||||
var DEBUG = false;
|
||||
var log = [];
|
||||
|
||||
// Die Version steht ausschliesslich im Metadatenblock. Ein zweiter Literal
|
||||
// im Code lief beim Release regelmaessig davon weg, ohne dass es auffiel —
|
||||
// GM_info liest denselben Header zurueck. Fehlt es (Userscript-Manager ohne
|
||||
// GM_info), bleibt das Overlay ohne Versionsangabe, statt eine falsche zu
|
||||
// zeigen.
|
||||
var VERSION = (typeof GM_info !== 'undefined' && GM_info.script &&
|
||||
GM_info.script.version) || '';
|
||||
|
||||
function h() { return document.head || document.documentElement; }
|
||||
|
||||
function txt(el) { return (el ? el.textContent || '' : '')
|
||||
.replace(/\s+/g, ' ').trim(); }
|
||||
|
||||
// input[type=submit]/[type=reset] behalten in iOS Safari trotz
|
||||
// -webkit-appearance:none teils ihre native Knopfoptik und setzen eigenes
|
||||
// Padding und Zentrierung nur unzuverlaessig um — ein echtes <button> wird
|
||||
// unter appearance:none dagegen zuverlaessig respektiert. name und value
|
||||
// wandern mit, damit sich am uebermittelten Formularfeld nichts aendert.
|
||||
function inputToButton(input) {
|
||||
if (!input) return null;
|
||||
var btn = document.createElement('button');
|
||||
btn.type = input.type;
|
||||
if (input.name) btn.name = input.name;
|
||||
if (input.hasAttribute('value')) btn.value = input.value;
|
||||
btn.className = input.className;
|
||||
btn.textContent = input.value;
|
||||
input.replaceWith(btn);
|
||||
return btn;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -35,6 +60,12 @@
|
||||
: '';
|
||||
log.push('ansicht=' + (view || 'keine'));
|
||||
|
||||
// Muss zum fruehestmoeglichen Zeitpunkt stehen, nicht erst beim Aufbau: iOS
|
||||
// Safari stellt die Scrollposition einer schon besuchten Adresse von sich aus
|
||||
// wieder her, und zwar bevor das Script ueberhaupt etwas gebaut hat. Ein
|
||||
// spaeterer Reset kaeme dagegen nur hinterher.
|
||||
if (view && 'scrollRestoration' in history) history.scrollRestoration = 'manual';
|
||||
|
||||
if (view && !document.querySelector('meta[name="viewport"][data-rm]')) {
|
||||
var m = document.createElement('meta');
|
||||
m.name = 'viewport';
|
||||
@@ -86,10 +117,21 @@
|
||||
color:#eee !important; text-decoration:none !important; }
|
||||
.rm-crumbs a:last-of-type { font-weight:650; }
|
||||
.rm-crumbs .rm-sep { flex:none; font-size:13px; color:#eee; opacity:.45; }
|
||||
.rm-crumbs a.rm-auth { flex:none; margin-left:auto; padding:6px 12px !important;
|
||||
/* Traegt die Lupe und die Login/Logout-Pille zusammen als eine Einheit
|
||||
rechts — nur eine gemeinsame margin-left:auto, statt jedes Element
|
||||
einzeln an den Rand zu schieben, sonst klafft zwischen beiden eine
|
||||
Luecke statt "direkt links neben der Pille". */
|
||||
.rm-crumbs .rm-auth-group { flex:none; margin-left:auto;
|
||||
display:flex; align-items:center; gap:6px; }
|
||||
.rm-crumbs a.rm-auth { flex:none; padding:6px 12px !important;
|
||||
font-size:13px !important; border-radius:99px;
|
||||
background:rgba(128,128,128,.18);
|
||||
border:1px solid rgba(160,160,160,.4); }
|
||||
.rm-crumbs a.rm-search { flex:none; min-width:44px; height:29px;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
padding:0 16px !important; font-size:14px; border-radius:99px;
|
||||
background:rgba(128,128,128,.18);
|
||||
border:1px solid rgba(160,160,160,.4); }
|
||||
/* Nur gesetzt, wenn die Leiste wirklich gebaut wurde — ohne Brotkrumen
|
||||
im Original-DOM bleibt der Platz sonst leer reserviert. */
|
||||
body.rm-crumbed .rm-root {
|
||||
@@ -97,6 +139,38 @@
|
||||
body.rm-crumbed .rm-dbg {
|
||||
margin-top:calc(45px + env(safe-area-inset-top)); }
|
||||
|
||||
/* Zweite Leiste unter den Brotkrumen, nur im Thread: Ersatz fuer
|
||||
"Search this topic..." der Originalseite. Das dortige Formular wird
|
||||
unveraendert hierher verschoben statt neu gebaut, wie das
|
||||
Login-Formular in .rm-login form. */
|
||||
.rm-search-bar { box-sizing:border-box;
|
||||
position:fixed; left:0; right:0; z-index:9997;
|
||||
top:calc(44px + env(safe-area-inset-top));
|
||||
display:flex; align-items:center;
|
||||
padding:8px 12px;
|
||||
background:rgba(28,28,30,.96);
|
||||
-webkit-backdrop-filter:blur(12px);
|
||||
backdrop-filter:blur(12px);
|
||||
border-bottom:1px solid rgba(128,128,128,.3);
|
||||
transform:translateY(-110%);
|
||||
transition:transform .18s ease; }
|
||||
.rm-search-bar.rm-open { transform:translateY(0); }
|
||||
.rm-search-bar form { flex:1; min-width:0;
|
||||
display:flex; align-items:center; gap:8px; }
|
||||
.rm-search-bar input[type="text"] {
|
||||
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-search-bar button[type="submit"] {
|
||||
-webkit-appearance:none; appearance:none;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
box-sizing:border-box; line-height:normal; text-align:center;
|
||||
flex:none; min-height:44px; font-size:14px; font-weight:650;
|
||||
padding:12px 16px;
|
||||
border-radius:10px; color:#fff; border:0;
|
||||
background:rgba(120,170,255,.85); }
|
||||
|
||||
/* === Themen- und Forenliste === */
|
||||
|
||||
.rm-sec { padding:18px 14px 6px; font-size:12px; font-weight:700;
|
||||
@@ -169,9 +243,13 @@
|
||||
font-size:21px; font-weight:650; line-height:1.3;
|
||||
overflow-wrap:break-word; }
|
||||
|
||||
/* Die Karte traegt seit dem Anker-Uebertrag die id des Beitrags und ist
|
||||
damit Sprungziel von #pXXXX. Ohne den Freiraum landet sie hinter der
|
||||
fixierten Brotkrumenleiste, der Kartenkopf waere verdeckt. */
|
||||
.rm-post { box-sizing:border-box !important;
|
||||
width:100% !important; max-width:100% !important;
|
||||
padding:14px !important;
|
||||
scroll-margin-top:calc(53px + env(safe-area-inset-top));
|
||||
border-top:1px solid rgba(128,128,128,.25) !important; }
|
||||
|
||||
.rm-head { display:flex; align-items:center; gap:9px;
|
||||
@@ -258,10 +336,104 @@
|
||||
text-decoration:none !important; color:inherit !important; }
|
||||
.rm-login form input[type="checkbox"] { width:18px; height:18px;
|
||||
vertical-align:middle; margin-right:8px; }
|
||||
.rm-login form input[type="submit"] {
|
||||
width:100%; font-size:16px; font-weight:650; padding:13px;
|
||||
margin-top:20px; border-radius:10px; color:#fff; border:0;
|
||||
.rm-login form button[type="submit"] {
|
||||
-webkit-appearance:none; appearance:none;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
box-sizing:border-box; line-height:normal; text-align:center;
|
||||
width:100%; min-height:48px; font-size:16px; font-weight:650;
|
||||
padding:16px 13px; margin-top:20px; border-radius:10px; color:#fff;
|
||||
border:0; background:rgba(120,170,255,.85); }
|
||||
|
||||
/* === Suchformular (search.php ohne Treffer) === */
|
||||
|
||||
.rm-search-shortcuts { display:flex; flex-wrap:wrap; gap:8px;
|
||||
padding:0 14px 16px; }
|
||||
.rm-search-shortcuts a { font-size:13px; padding:6px 12px; border-radius:99px;
|
||||
border:1px solid rgba(128,128,128,.4);
|
||||
text-decoration:none !important; color:inherit !important; }
|
||||
|
||||
/* Derselbe Tabelle-zu-Block-Trick wie bei .rm-login form, eigener Name
|
||||
statt Wiederverwendung: das Formular hier ist ein anderes und hat
|
||||
andere Feldtypen (radio, select statt checkbox). */
|
||||
.rm-searchform form { display:block;
|
||||
padding:4px 14px calc(80px + env(safe-area-inset-bottom)); }
|
||||
.rm-searchform form table, .rm-searchform form tbody,
|
||||
.rm-searchform form tr, .rm-searchform form td {
|
||||
display:block !important; width:100% !important; }
|
||||
.rm-searchform form td { padding:0 !important; }
|
||||
/* Bleibt sichtbar statt wie beim Login versteckt: die beiden th-Zeilen
|
||||
("Search query", "Search options") sind hier Abschnittstrenner, keine
|
||||
Formular-Kopfzelle ohne eigenen Inhalt. */
|
||||
.rm-searchform form th { display:block !important; padding:16px 0 4px;
|
||||
font-size:12px; font-weight:700;
|
||||
text-transform:uppercase; letter-spacing:.06em;
|
||||
opacity:.55; }
|
||||
.rm-searchform form b.genmed { display:block; margin:14px 0 4px;
|
||||
font-size:14px; font-weight:650; }
|
||||
.rm-searchform form span.gensmall { display:block; margin-bottom:6px;
|
||||
font-size:12px; opacity:.6; }
|
||||
.rm-searchform form input[type="text"] {
|
||||
box-sizing:border-box; width:100%; font-size:16px; padding:11px 12px;
|
||||
border-radius:10px; color:#eee; background:rgba(120,120,120,.18);
|
||||
border:1px solid rgba(160,160,160,.35); }
|
||||
.rm-searchform form select {
|
||||
box-sizing:border-box; width:100%; font-size:16px; padding:9px 12px;
|
||||
margin-top:4px; border-radius:10px; color:#eee;
|
||||
background:rgba(120,120,120,.18); border:1px solid rgba(160,160,160,.35); }
|
||||
.rm-searchform form select[multiple] { font-size:15px; padding:4px; }
|
||||
.rm-searchform form input[type="radio"] { width:18px; height:18px;
|
||||
vertical-align:middle; margin-right:6px; }
|
||||
/* Einzige Zeile ohne Block-Stapelung: Suchen/Zuruecksetzen sollen
|
||||
nebeneinander stehen, nicht wie der Rest des Formulars untereinander. */
|
||||
.rm-searchform form td.cat { display:flex !important; gap:10px;
|
||||
padding-top:10px !important; }
|
||||
/* input[type=submit]/[type=reset] behalten in iOS Safari trotz
|
||||
-webkit-appearance:none teils die native Knopfoptik und setzen Padding,
|
||||
Rahmen und Zentrierung nur unzuverlaessig um — deshalb echte <button>
|
||||
statt der Original-<input>-Elemente, siehe inputToButton(). */
|
||||
.rm-searchform form button[type="submit"],
|
||||
.rm-searchform form button[type="reset"] {
|
||||
-webkit-appearance:none; appearance:none;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
box-sizing:border-box; line-height:normal; text-align:center;
|
||||
min-height:48px; font-size:16px; font-weight:650;
|
||||
padding:16px 13px; border-radius:10px;
|
||||
border:0; }
|
||||
.rm-searchform form button[type="submit"] { flex:1; color:#fff;
|
||||
background:rgba(120,170,255,.85); }
|
||||
.rm-searchform form button[type="reset"] { flex:none; color:#eee;
|
||||
background:rgba(120,120,120,.18);
|
||||
border:1px solid rgba(160,160,160,.45) !important; }
|
||||
|
||||
/* === Suchtreffer (search.php mit Ergebnissen, Modus "Posts") === */
|
||||
|
||||
.rm-hits-info { padding:0 14px 14px; font-size:13px; opacity:.75;
|
||||
overflow-wrap:break-word; }
|
||||
.rm-hits-info a { color:inherit !important; }
|
||||
|
||||
.rm-hit { box-sizing:border-box !important; display:block !important;
|
||||
width:100% !important; padding:14px !important;
|
||||
border-top:1px solid rgba(128,128,128,.25) !important; }
|
||||
.rm-hit-ctx { display:block; margin-bottom:8px; font-size:12px;
|
||||
font-weight:650; text-transform:uppercase; letter-spacing:.03em;
|
||||
opacity:.6; text-decoration:none !important;
|
||||
color:inherit !important; overflow-wrap:break-word; }
|
||||
.rm-hit-head { display:flex; align-items:baseline; justify-content:space-between;
|
||||
gap:8px; }
|
||||
.rm-hit-user { font-size:14px; font-weight:650; }
|
||||
.rm-hit-when { flex:none; font-size:12px; opacity:.55; }
|
||||
/* Wiederverwendet: dieselbe .rm-meta-Regel aus der Themenliste, gleiche
|
||||
"X Antworten · Y Views"-Optik. */
|
||||
.rm-hit .rm-meta { margin-top:4px; }
|
||||
/* .rm-body wird 1:1 vom Thread uebernommen (gleiches Beitragsmarkup),
|
||||
nur der verschobene Container ist hier ein td statt div/span und
|
||||
braucht die Umstellung auf Block, die .rm-body selbst nicht mitbringt. */
|
||||
.rm-root.rm-hits .rm-body { margin-top:8px; }
|
||||
.rm-root.rm-hits .rm-body > td { display:block !important; padding:0 !important; }
|
||||
.rm-hit-go { display:inline-block; margin-top:10px; font-size:12px;
|
||||
padding:5px 10px; border:1px solid rgba(128,128,128,.4);
|
||||
border-radius:99px; text-decoration:none !important;
|
||||
color:inherit !important; }
|
||||
|
||||
/* === Seitennavigation, beide Ansichten === */
|
||||
|
||||
@@ -388,11 +560,31 @@
|
||||
if (!n) return;
|
||||
|
||||
var st = loginState();
|
||||
var group = document.createElement('div');
|
||||
group.className = 'rm-auth-group';
|
||||
|
||||
// Lupe nur bei bestehender Sitzung. Ausserhalb von viewtopic fuehrt sie
|
||||
// direkt auf search.php, im Thread blendet sie stattdessen die zweite
|
||||
// Leiste ein — Ersatz fuer "Search this topic...", das mit dem
|
||||
// Original-DOM ausgeblendet wurde.
|
||||
if (st.in) {
|
||||
var search = document.createElement('a');
|
||||
search.className = 'rm-search';
|
||||
search.href = 'https://cs.rin.ru/forum/search.php';
|
||||
search.textContent = '🔍';
|
||||
search.setAttribute('aria-label', 'Suche');
|
||||
search.addEventListener('click', function (e) {
|
||||
if (view === 'thread') { e.preventDefault(); toggleThreadSearch(); }
|
||||
});
|
||||
group.appendChild(search);
|
||||
}
|
||||
|
||||
var auth = document.createElement('a');
|
||||
auth.className = 'rm-auth';
|
||||
auth.href = st.href;
|
||||
auth.textContent = st.in ? (st.name || 'Logout') : 'Login';
|
||||
bar.appendChild(auth);
|
||||
group.appendChild(auth);
|
||||
bar.appendChild(group);
|
||||
|
||||
document.body.appendChild(bar);
|
||||
document.body.classList.add('rm-crumbed');
|
||||
@@ -946,7 +1138,199 @@
|
||||
return n;
|
||||
}
|
||||
|
||||
// search.php ohne Treffer zeigt nur das Suchformular — keine topictitle-
|
||||
// oder forumlink-Zeilen, an denen buildList() sonst seine Karten festmacht.
|
||||
// Erkannt wird das Formular am Forenwaehler select[name="fid[]"]: der
|
||||
// steckt nur im grossen Suchformular, nicht in den kleinen "Search this
|
||||
// forum/topic..."-Formularen auf Foren- und Themenseiten, die denselben
|
||||
// Formularnamen "keywords" verwenden wuerden und sonst faelschlich
|
||||
// getroffen werden koennten.
|
||||
function buildSearchForm() {
|
||||
var picker = document.querySelector('select[name="fid[]"]');
|
||||
var form = picker && picker.closest('form');
|
||||
if (!form) return false;
|
||||
|
||||
// Vorbelegung fuer die mobile Nutzung: Main Forum (Forum-ID 10, siehe
|
||||
// dev/viewforum.php) vorausgewaehlt statt "kein Forum", Unterforen dabei
|
||||
// nicht automatisch mitgesucht — das serverseitige Formular startet sonst
|
||||
// mit leerem Forenwaehler und "Search subforums: Yes".
|
||||
var mainForum = picker.querySelector('option[value="10"]');
|
||||
if (mainForum) mainForum.selected = true;
|
||||
var noSubforums = form.querySelector('input[name="sc"][value="0"]');
|
||||
if (noSubforums) noSubforums.checked = true;
|
||||
|
||||
var root = document.createElement('div');
|
||||
root.className = 'rm-root rm-searchform';
|
||||
|
||||
var title = document.createElement('div');
|
||||
title.className = 'rm-login-title';
|
||||
title.textContent = 'Suche';
|
||||
root.appendChild(title);
|
||||
|
||||
// Schnellzugriffe (View new posts, View your posts, ...) stehen als
|
||||
// eigene Linkliste ausserhalb des Formulars und würden sonst mit dem Rest
|
||||
// der Originalseite verschwinden.
|
||||
var quick = document.querySelector('p.searchbar');
|
||||
if (quick) {
|
||||
var links = quick.querySelectorAll('a');
|
||||
if (links.length) {
|
||||
var chips = document.createElement('div');
|
||||
chips.className = 'rm-search-shortcuts';
|
||||
links.forEach(function (a) {
|
||||
var c = document.createElement('a');
|
||||
c.href = a.href;
|
||||
c.textContent = txt(a);
|
||||
chips.appendChild(c);
|
||||
});
|
||||
root.appendChild(chips);
|
||||
}
|
||||
}
|
||||
|
||||
// Die Textfelder tragen ein inline style="width: 300px", das jede externe
|
||||
// Breitenregel ohne !important sticht.
|
||||
form.querySelectorAll('input[type="text"]').forEach(function (inp) {
|
||||
inp.removeAttribute('style');
|
||||
});
|
||||
|
||||
form.querySelectorAll('input[type="submit"], input[type="reset"]')
|
||||
.forEach(inputToButton);
|
||||
|
||||
root.appendChild(form);
|
||||
replaceBody(root);
|
||||
addCrumbs();
|
||||
log.push('suche=formular');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Eine Suche im Modus "Posts" (Standard beim Suchen ueber die zweite Leiste
|
||||
// im Thread, da deren Formular kein sr setzt) liefert etwas ganz anderes
|
||||
// als eine Themenliste: pro Treffer Autor, Zeitstempel und ein Auszug aus
|
||||
// dem Beitrag selbst, keine topictitle-/forumlink-Zeilen. buildList() wuerde
|
||||
// die darin enthaltenen viewtopic.php-Links trotzdem einsammeln und an
|
||||
// rowKind() falsch klassifizieren, ohne diese eigene Behandlung davor.
|
||||
// Erkannt wird der Treffer-Modus an p.topictitle — als <p>, nicht als <a>
|
||||
// wie das gleichnamige forumlink-Gegenstueck einer Themenzeile.
|
||||
function buildSearchResults() {
|
||||
var hits = document.querySelectorAll('p.topictitle');
|
||||
if (!hits.length) return false;
|
||||
|
||||
var root = document.createElement('div');
|
||||
root.className = 'rm-root rm-hits';
|
||||
|
||||
var summary = document.querySelector('span.titles');
|
||||
var title = document.createElement('div');
|
||||
title.className = 'rm-login-title';
|
||||
title.textContent = summary ? txt(summary) : 'Suchergebnisse';
|
||||
root.appendChild(title);
|
||||
|
||||
// "Searched topic: ... / Search term used: ..." traegt funktionierende
|
||||
// Links (erneute Suche, Sprung zum Thema) und wird deshalb wie das
|
||||
// Formular selbst uebernommen statt aus dem Text nachgebaut.
|
||||
var form = hits[0].closest('form');
|
||||
var infoCell = form && form.querySelector('td.genmed');
|
||||
if (infoCell) {
|
||||
infoCell.classList.add('rm-hits-info');
|
||||
root.appendChild(infoCell);
|
||||
}
|
||||
|
||||
var built = 0;
|
||||
hits.forEach(function (p) {
|
||||
try {
|
||||
var card = buildHit(p);
|
||||
if (card) { root.appendChild(card); built++; }
|
||||
} catch (e) { log.push('treffer: ' + e.message); }
|
||||
});
|
||||
log.push('treffer=' + built);
|
||||
if (!built) return false;
|
||||
|
||||
// Muss vor replaceBody() laufen: pageState() liest das Original-DOM.
|
||||
var pg = pageState();
|
||||
if (pg.total > 1) pageNav(root, pg);
|
||||
|
||||
replaceBody(root);
|
||||
addCrumbs();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ein Treffer besteht aus drei aufeinanderfolgenden <tr> derselben Tabelle:
|
||||
// Titelzeile (Forum/Thema), Autor- und Betreffzeile, Antworten/Views samt
|
||||
// Beitragsauszug. Die Zuordnung laeuft ueber nextElementSibling statt ueber
|
||||
// Klassen, weil alle drei row1/row2-Zeilen sonst gleich aussehen.
|
||||
function buildHit(p) {
|
||||
var titleRow = p.closest('tr');
|
||||
var infoRow = titleRow && titleRow.nextElementSibling;
|
||||
var bodyRow = infoRow && infoRow.nextElementSibling;
|
||||
|
||||
var forumA = p.querySelector('a[href*="viewforum.php"]');
|
||||
var topicA = p.querySelector('a[href*="viewtopic.php"]');
|
||||
if (!topicA) return null;
|
||||
|
||||
var el = document.createElement('div');
|
||||
el.className = 'rm-hit';
|
||||
|
||||
var ctx = document.createElement('a');
|
||||
ctx.className = 'rm-hit-ctx';
|
||||
ctx.href = topicA.href;
|
||||
ctx.textContent = breakable((forumA ? txt(forumA) + ' › ' : '') + txt(topicA));
|
||||
el.appendChild(ctx);
|
||||
|
||||
var author = '', posted = '', subjectA = null;
|
||||
if (infoRow) {
|
||||
var ua = infoRow.querySelector('.postauthor');
|
||||
author = ua ? txt(ua) : '';
|
||||
subjectA = infoRow.querySelector('a[href*="viewtopic.php"]');
|
||||
var pm = txt(infoRow).match(/Posted:\s*(.+)$/i);
|
||||
if (pm) posted = pm[1].trim();
|
||||
}
|
||||
|
||||
var head = document.createElement('div');
|
||||
head.className = 'rm-hit-head';
|
||||
var who = document.createElement('span');
|
||||
who.className = 'rm-hit-user';
|
||||
who.textContent = author || 'Gast';
|
||||
head.appendChild(who);
|
||||
if (posted) {
|
||||
var when = document.createElement('span');
|
||||
when.className = 'rm-hit-when';
|
||||
when.textContent = posted;
|
||||
head.appendChild(when);
|
||||
}
|
||||
el.appendChild(head);
|
||||
|
||||
if (bodyRow) {
|
||||
// txt() haengt "Replies:" und "Views:" ohne Trennzeichen aneinander,
|
||||
// weil das <br /> dazwischen keinen Leerraum hinterlaesst — deshalb
|
||||
// einzeln herausgegriffen und ueber dieselbe metaRow() wie bei
|
||||
// Themenkarten mit " · " verbunden.
|
||||
var metaTxt = txt(bodyRow.querySelector('.postdetails'));
|
||||
var reM = metaTxt.match(/Replies:\s*([\d.,]+)/i);
|
||||
var viM = metaTxt.match(/Views:\s*([\d.,]+)/i);
|
||||
var metaEl = metaRow([reM ? reM[1] + ' Antworten' : '',
|
||||
viM ? viM[1] + ' Views' : '']);
|
||||
if (metaEl) el.appendChild(metaEl);
|
||||
|
||||
var pb = bodyRow.querySelector('.postbody');
|
||||
if (pb) {
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'rm-body';
|
||||
wrap.appendChild(pb);
|
||||
el.appendChild(wrap);
|
||||
}
|
||||
}
|
||||
|
||||
var jump = document.createElement('a');
|
||||
jump.className = 'rm-hit-go';
|
||||
jump.href = (subjectA || topicA).href;
|
||||
jump.textContent = 'Zum Beitrag ↓';
|
||||
el.appendChild(jump);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function buildList() {
|
||||
if (buildSearchForm()) return;
|
||||
if (buildSearchResults()) return;
|
||||
|
||||
// Dokumentreihenfolge entscheidet: in einer Forenzeile steht der Forenname
|
||||
// vor dem Link auf den letzten Beitrag. Ohne diese Reihenfolge faellt die
|
||||
// Zeile als Thema an, der Forenname verschwindet und nur die Beschreibung
|
||||
@@ -1032,6 +1416,47 @@
|
||||
// Thread
|
||||
// ==========================================================================
|
||||
|
||||
// "Search this topic..." steht dreifach identisch im Original-DOM (Kopf,
|
||||
// Fuss, ueber der Beitragsliste), samt versteckter Felder fuer Themen-Id
|
||||
// und Scope (sf=msgonly). replaceBody() versteckt das nur per Klasse, das
|
||||
// erste Formular bleibt im DOM erreichbar und wird beim ersten Antippen
|
||||
// der Lupe unveraendert in die zweite Leiste verschoben statt neu gebaut —
|
||||
// wie das Login-Formular in buildLogin().
|
||||
var threadSearchBar = null;
|
||||
|
||||
function buildThreadSearch() {
|
||||
var form = document.querySelector('#topic-search');
|
||||
if (!form) return null;
|
||||
|
||||
// Die Originalseite emuliert einen Platzhalter per onclick/onblur statt
|
||||
// per placeholder-Attribut.
|
||||
var inp = form.querySelector('input[name="keywords"]');
|
||||
if (inp) {
|
||||
inp.removeAttribute('onclick');
|
||||
inp.removeAttribute('onblur');
|
||||
if (inp.value === 'Search this topic…') inp.value = '';
|
||||
inp.placeholder = 'In diesem Thema suchen';
|
||||
}
|
||||
var submit = form.querySelector('input[type="submit"]');
|
||||
if (submit) { submit.value = 'Suchen'; inputToButton(submit); }
|
||||
|
||||
var bar = document.createElement('div');
|
||||
bar.className = 'rm-search-bar';
|
||||
bar.appendChild(form);
|
||||
document.body.appendChild(bar);
|
||||
return bar;
|
||||
}
|
||||
|
||||
function toggleThreadSearch() {
|
||||
if (!threadSearchBar) threadSearchBar = buildThreadSearch();
|
||||
if (!threadSearchBar) return;
|
||||
var open = threadSearchBar.classList.toggle('rm-open');
|
||||
if (open) {
|
||||
var inp = threadSearchBar.querySelector('input[type="text"]');
|
||||
if (inp) inp.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -1087,6 +1512,16 @@
|
||||
pr = pr.previousElementSibling;
|
||||
}
|
||||
|
||||
// --- Sprungziel ---
|
||||
// Der Anker eines Beitrags steht als <a name="pXXXX"> in der Autorenzelle
|
||||
// und wandert mit dem Original-DOM in die Unsichtbarkeit. Ohne ihn auf der
|
||||
// Karte hat eine Adresse mit #pXXXX kein Ziel mehr, und der Browser bleibt
|
||||
// stehen, wo er beim Umbau gerade stand.
|
||||
var anc = null;
|
||||
for (var ci = 0; ci < cells.length && !anc; ci++)
|
||||
anc = cells[ci].querySelector('a[name^="p"]');
|
||||
if (anc) card.id = anc.getAttribute('name');
|
||||
|
||||
// --- Username ---
|
||||
var user = '';
|
||||
for (var i = 0; i < cells.length && !user; i++) {
|
||||
@@ -1234,6 +1669,8 @@
|
||||
if (!td.children.length && !txt(td)) td.classList.add('rm-hidden');
|
||||
});
|
||||
|
||||
inputToButton(form.querySelector('input[type="submit"]'));
|
||||
|
||||
root.appendChild(form);
|
||||
replaceBody(root);
|
||||
addCrumbs();
|
||||
@@ -1277,6 +1714,46 @@
|
||||
|
||||
// ==========================================================================
|
||||
|
||||
// Der Umbau laeuft erst auf DOMContentLoaded, also lange nachdem der Browser
|
||||
// seine Scrollposition festgelegt hat — und replaceBody() ruehrt sie nicht an.
|
||||
// Weil ein Thread auch nach dem Umbau laenger als der Viewport bleibt, klemmt
|
||||
// der Browser sie auch nicht auf 0 zurueck: der Thread startet dann mitten im
|
||||
// ersten Beitrag statt an seinem Anfang. Auf dem Geraet faellt das auf, im
|
||||
// Chrome-Debugging nicht, weil dort jede Adresse frisch geladen wird und es
|
||||
// gar keine Position zu uebernehmen gibt.
|
||||
function toStart() {
|
||||
var id = location.hash ? location.hash.slice(1) : '';
|
||||
var target = id ? document.getElementById(id) : null;
|
||||
if (target) target.scrollIntoView();
|
||||
else window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
// Einmal zuruecksetzen genuegt nicht: Safari legt die Position nicht in einem
|
||||
// Rutsch fest, sondern springt nach 'load' und nach dem Layout nachgeladener
|
||||
// Bilder noch einmal nach — ein Reset auf DOMContentLoaded wird davon einfach
|
||||
// ueberschrieben. Der Anfang wird deshalb kurz festgehalten. Die Bremse loest
|
||||
// bei der ersten eigenen Geste des Lesers sofort aus, sonst risse sie ihn
|
||||
// waehrend des Lesens zurueck; 'scroll' taugt dafuer nicht als Signal, weil
|
||||
// toStart() selbst eines ausloest.
|
||||
function holdStart() {
|
||||
var free = false;
|
||||
function release() { free = true; }
|
||||
['touchstart', 'wheel', 'keydown'].forEach(function (ev) {
|
||||
window.addEventListener(ev, release, { passive: true, once: true });
|
||||
});
|
||||
|
||||
var tries = 0;
|
||||
(function tick() {
|
||||
if (free) return;
|
||||
toStart();
|
||||
if (++tries < 12) setTimeout(tick, 50);
|
||||
})();
|
||||
|
||||
// Faengt den Sprung ab, der erst nach dem letzten Bild kommt und damit
|
||||
// jenseits der 600 ms der Schleife liegen kann.
|
||||
window.addEventListener('load', function () { if (!free) toStart(); });
|
||||
}
|
||||
|
||||
function run() {
|
||||
try {
|
||||
if (view === 'liste') buildList();
|
||||
@@ -1284,10 +1761,13 @@
|
||||
else if (view === 'login') buildLogin();
|
||||
} catch (e) { log.push('build: ' + e.message); }
|
||||
|
||||
if (view && document.querySelector('.rm-root')) holdStart();
|
||||
|
||||
if (DEBUG) {
|
||||
var d = document.createElement('div');
|
||||
d.className = 'rm-dbg';
|
||||
d.textContent = 'RIN Mobile v3.8\n' + log.join('\n');
|
||||
d.textContent = 'RIN Mobile' + (VERSION ? ' v' + VERSION : '') +
|
||||
'\n' + log.join('\n');
|
||||
document.body.insertBefore(d, document.body.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user