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
|
# 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` —
|
mobile Ansicht umbaut. Kein Build, keine Abhaengigkeiten, kein `package.json` —
|
||||||
die `.user.js`-Datei ist 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/*` | Beide Ansichten, Weiche nach Seitentyp |
|
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Alle Ansichten, Weiche nach Seitentyp |
|
||||||
| `dev/rin-mobile.dev.user.js` | zusaetzlich `localhost:*` | Loader, bindet per `@require file:///` das echte Script ein |
|
| `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, 1114 Seiten |
|
| `dev/viewtopic.php` | — | Fixture: gespeicherter Thread, `per_page=15`, 1114 Seiten |
|
||||||
| `dev/viewforum.php` | — | Fixture: Main Forum, 610 Seiten, mit Unterforen, Ankuendigungen und Stickies |
|
| `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 |
|
| `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/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
|
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
|
frischen Klon sind sie nicht da und muessen nach der Anleitung in der README neu
|
||||||
gespeichert werden — die Seite verlangt einen Login.
|
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
|
## Aufbau
|
||||||
|
|
||||||
Ein IIFE, streng der Reihe nach:
|
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
|
5. `pageState()`, `pageSheet()` und `pageNav()` — die Paginierung, von Liste
|
||||||
und Thread benutzt, nicht vom Login
|
und Thread benutzt, nicht vom Login
|
||||||
6. `buildList()`, `buildThread()` beziehungsweise `buildLogin()` lesen das
|
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
|
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
|
7. `run()` waehlt die Ansicht und haengt bei `DEBUG` das gruene Overlay an
|
||||||
8. Start ueber `DOMContentLoaded`, falls `readyState === 'loading'`
|
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
|
Klassennamen aus der phpBB-Dokumentation (`forumlink`, `forumdesc`) deshalb
|
||||||
zunaechst nur eine Vermutung; `dev/viewforum.php` bestaetigt sie inzwischen. Der
|
zunaechst nur eine Vermutung; `dev/viewforum.php` bestaetigt sie inzwischen. Der
|
||||||
Rueckfall auf die Form der Zeile bleibt trotzdem stehen — er kostet nichts und
|
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
|
**Die Brotkrumenleiste ist fixiert, nicht sticky.** `.rm-root` traegt
|
||||||
`overflow:hidden` und wuerde damit zum Scrollport einer `position:sticky`
|
`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.**
|
**Die Login-Erkennung ist inzwischen gegen eine echte Sitzung geprueft.**
|
||||||
`loginState()` unterscheidet `a[href*="mode=login"]` von
|
`loginState()` unterscheidet `a[href*="mode=login"]` von
|
||||||
`a[href*="mode=logout"]` im Menuebalken und liest den Namen aus dem Muster
|
`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`
|
`Logout [ Name ]` im Linktext. `dev/index.php`, `dev/search.php` und
|
||||||
sind eingeloggt aufgenommen (`Logout [ <Benutzername> ]`) und bestaetigen das Muster.
|
`dev/search2.php` sind eingeloggt aufgenommen (`Logout [ <Benutzername> ]`) und
|
||||||
|
bestaetigen das Muster.
|
||||||
Der Fallback bleibt trotzdem stehen: passt das Muster einmal nicht — andere
|
Der Fallback bleibt trotzdem stehen: passt das Muster einmal nicht — andere
|
||||||
Sprache, anderer Klammertyp — zeigt die Pille „Logout" ohne Namen, statt eine
|
Sprache, anderer Klammertyp — zeigt die Pille „Logout" ohne Namen, statt eine
|
||||||
Ausnahme zu werfen.
|
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
|
an ihrer Form — eine Kopfzeile mit genau einer `th`-Zelle, gefolgt von einer
|
||||||
`td.row1`-Nachrichtenzelle — nicht am Wortlaut, und uebernimmt die Zelle
|
`td.row1`-Nachrichtenzelle — nicht am Wortlaut, und uebernimmt die Zelle
|
||||||
unveraendert wie das Formular selbst. Findet `buildLogin()` weder Formular
|
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
|
**`index.php` wird ueber `location.pathname` erkannt, nicht ueber
|
||||||
`here`.** Der Login-Redirect haengt "index.php" gelegentlich als Wert eines
|
`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
|
einer verschachtelten Tabelle, `closest('tr')` trifft die innere Zeile ohne
|
||||||
Autor- und Profilspalte.
|
Autor- und Profilspalte.
|
||||||
|
|
||||||
**`DEBUG` steht auf `true`** und muss vor dem Ausliefern auf `false`. Die
|
**Der Umbau erbt die Scrollposition, die der Browser vorher gesetzt hat.**
|
||||||
Versionsnummer steht doppelt: im `@version`-Header und im Text des
|
`replaceBody()` laeuft auf `DOMContentLoaded` und ruehrt `scrollY` nicht an;
|
||||||
Debug-Overlays.
|
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
|
## Testen
|
||||||
|
|
||||||
@@ -229,6 +396,22 @@ Drei Stolpersteine dabei:
|
|||||||
Sinnvolle Faelle: erste Seite, zweite Seite, Mitte, letzte Seite, dazu die
|
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`.
|
||||||
|
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
|
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
|
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.
|
die Zahlenspalten dasselbe Ergebnis liefern.
|
||||||
|
|
||||||
Fuer den Login genuegt `dev/ucp.php` unveraendert: geprueft werden das
|
Fuer den Login genuegt `dev/ucp.php` unveraendert: geprueft werden das
|
||||||
uebernommene Formular (Username-/Passwortfeld, `sid`, `redirect`, Submit) und
|
uebernommene Formular (Username-/Passwortfeld, `sid`, `redirect`, Submit), der
|
||||||
die Pille im ausgeloggten Zustand. Fuer den eingeloggten Zustand der Pille
|
Regeltext als zugeklapptes `details` („Regeln") und die Pille im ausgeloggten
|
||||||
`dev/index.php` oder `dev/after_login/ucp.php` laden — beide sind eingeloggt
|
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.
|
aufgenommen und tragen das echte `Logout [ Name ]`-Markup.
|
||||||
|
|
||||||
Fuer die Meldung nach dem Login genuegt `dev/after_login/ucp.php`
|
Die Meldung nach dem Login (`buildLoginInfo()`) ist mangels Fixture nicht
|
||||||
unveraendert, URL `ucp.php?mode=login`: geprueft werden Titel, uebernommene
|
nachpruefbar. Wer sie anfasst, nimmt `ucp.php?mode=login` nach einem echten
|
||||||
Nachricht samt Link und die Pille. Zu pruefen ist ausserdem, dass `dev/ucp.php`
|
Login neu auf; zu pruefen waeren dann Titel, uebernommene Nachricht samt Link
|
||||||
(das Formular, gleiche URL) weiterhin die Formular-Zweig traf und nicht in die
|
und die Pille — und dass `dev/ucp.php` (das Formular, gleiche URL) weiterhin
|
||||||
Meldungs-Erkennung faellt.
|
im Formular-Zweig landet und nicht in die Meldungs-Erkennung faellt.
|
||||||
|
|
||||||
Fuer die Forumwurzel genuegt `dev/index.php`, URL `index.php?sid=…`: zwei
|
Fuer die Forumwurzel genuegt `dev/index.php`, URL `index.php?sid=…`: zwei
|
||||||
Kategorien mit zusammen 16 Forenkarten, keine Paginierung. Zu pruefen ist
|
Kategorien mit zusammen 16 Forenkarten, keine Paginierung. Zu pruefen ist
|
||||||
zusaetzlich eine `ucp.php?mode=login&redirect=index.php`-URL — sie muss trotz
|
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.
|
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-*)`,
|
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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# iOS Safari Userscripts — cs.rin.ru
|
# RIN Mobile — cs.rin.ru for iOS Safari
|
||||||
|
|
||||||
[English](#english) | [Deutsch](#deutsch)
|
[English](#english) | [Deutsch](#deutsch)
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## English
|
## 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
|
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>`.
|
||||||
|
|
||||||
@@ -15,82 +15,57 @@ page type:
|
|||||||
|
|
||||||
| Page | View |
|
| 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 |
|
| `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 |
|
| `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 |
|
| anything else | Untouched — no styles are injected |
|
||||||
|
|
||||||
A `viewforum.php` page is not necessarily a list of topics: a category page
|
### Screenshots
|
||||||
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.
|
|
||||||
|
|
||||||
Forums, global announcements, announcements, stickies and topics all end up in
|
| Forum list, with the sections above the topics collapsed | Thread, with the in-topic search bar revealed |
|
||||||
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
|
| <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%"> |
|
||||||
inserted wherever the kind changes, and forum cards additionally carry a
|
| **Search form, top: quick links and forum picker** | **Search form, bottom: options and buttons** |
|
||||||
coloured left edge. The label is taken from the board's own separator row
|
| <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%"> |
|
||||||
(`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.
|
|
||||||
|
|
||||||
Tapping a header collapses its section. Global announcements, announcements and
|
### What it does
|
||||||
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.
|
|
||||||
|
|
||||||
Both views share the same fixed navigation bar at the bottom: first / previous /
|
The original page is hidden and a mobile layout is built from the same data,
|
||||||
next / last and a page indicator. Tapping the indicator opens a sheet with a
|
one view per page type:
|
||||||
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.
|
|
||||||
|
|
||||||
Both views carry a fixed breadcrumb bar at the top, taken from the forum's own
|
- **Lists as cards.** Forum and topic rows become cards with title, description,
|
||||||
`Board index » … » Main Forum` trail. Without it there is no way back out of a
|
counts and last post; category headers become section headings.
|
||||||
thread, because the original page is hidden and phpBB links the parent forum
|
- **Section strips.** Forums, announcements, stickies and topics are separated
|
||||||
nowhere else. The bar scrolls horizontally if the trail is too long for the
|
by a header carrying the section name and its card count. Announcements and
|
||||||
screen, and is left out entirely on pages that do not carry a trail.
|
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
|
The script's own labels are German; anything taken from the board keeps its
|
||||||
touches, not only the login form itself. Session state is read from the
|
original wording — `FOREN` in the first screenshot is the script's name for a
|
||||||
forum's own menu bar: a `mode=login` link means logged out, a `mode=logout`
|
group the board does not label, while `STICKIES` and `TOPICS` are the board's
|
||||||
link means logged in, and the pill switches between a "Login" link and the
|
own words.
|
||||||
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 login form itself is not rebuilt from scratch: the original `<form>`
|
It runs at `document-start` and injects a mobile viewport meta tag. A `DEBUG`
|
||||||
element is moved into the new layout and restyled, table rows collapsed to a
|
flag in the source, off by default, adds a green diagnostic overlay showing the
|
||||||
vertical stack. Reusing the element rather than reconstructing it keeps the
|
version, the chosen view and how many rows were converted.
|
||||||
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.
|
|
||||||
|
|
||||||
### Installation on iOS
|
### Installation on iOS
|
||||||
|
|
||||||
@@ -114,7 +89,7 @@ Tampermonkey.
|
|||||||
The loader in `dev/` references the script 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/rin-mobile-userscript/rin-mobile.user.js
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
@@ -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.
|
in `dev/rin-mobile.dev.user.js` to your own location.
|
||||||
|
|
||||||
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/rin-mobile-userscript/rin-mobile.user.js`. A `@require`
|
||||||
that cannot be resolved causes Tampermonkey to skip the script **entirely and
|
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
|
without an error message on the page**, which looks exactly like the script not
|
||||||
being installed at all.
|
being installed at all.
|
||||||
@@ -150,26 +125,28 @@ cards and two debug overlays.
|
|||||||
|
|
||||||
#### Test fixtures
|
#### Test fixtures
|
||||||
|
|
||||||
`cs.rin.ru` requires a login, so save the pages you need once and iterate
|
Layout work happens on the live site, with the loader active in Chrome. The
|
||||||
against local copies:
|
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>` →
|
1. Open the page while logged in, then in DevTools right-click `<html>` →
|
||||||
*Copy outerHTML*.
|
*Copy outerHTML*.
|
||||||
2. Save it as `dev/viewtopic.php.html` or `dev/viewforum.php.html`.
|
2. Save it under the page's own name — `dev/viewtopic.php`, `dev/viewforum.php`,
|
||||||
3. Serve the folder: `python -m http.server 8777 --directory .`
|
`dev/index.php`, `dev/ucp.php`, `dev/search.php`.
|
||||||
|
|
||||||
The file name matters twice over. The `.html` extension makes the server send
|
The loader deliberately matches `cs.rin.ru` only, so these files are not meant
|
||||||
the file as HTML instead of offering it as a download, and the retained
|
to be opened in the browser. Serving them locally would take `@include` rules
|
||||||
`viewtopic.php` / `viewforum.php` part is what the loader's `@include` rules
|
for `localhost`, and those match on page names common enough (`index.php`,
|
||||||
and the page-type check in `rin-mobile.user.js` match against.
|
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
|
A snapshot saved while logged out contains `span.link_removed` placeholders
|
||||||
layout work, because the script rebuilds the DOM anyway — only avatars stay
|
instead of links, and may use different markup for author names than the
|
||||||
empty, since their `src` is carried over unchanged.
|
logged-in view — worth knowing before puzzling over a fixture that behaves
|
||||||
|
differently from the real page.
|
||||||
Note that a snapshot saved while logged out contains `span.link_removed`
|
|
||||||
placeholders instead of links, and may use different markup for author names
|
|
||||||
than the logged-in view.
|
|
||||||
|
|
||||||
#### Device emulation
|
#### Device emulation
|
||||||
|
|
||||||
@@ -195,32 +172,35 @@ Windows (`npx playwright install webkit`).
|
|||||||
|
|
||||||
#### Troubleshooting
|
#### Troubleshooting
|
||||||
|
|
||||||
Install `dev/tm-test.user.js` — it injects a red bar and nothing else. If the
|
If nothing happens at all, separate the two possible causes before reading any
|
||||||
bar does not appear, the problem is Tampermonkey (see setup above), not the
|
code. Check the loader's *Externals* tab in the Tampermonkey dashboard: an
|
||||||
script code. If it does appear, check the loader's *Externals* tab in the
|
empty or failed entry means the `@require` path is wrong. If the entry is
|
||||||
Tampermonkey dashboard: an empty or failed entry means the `@require` path is
|
there and the script still does nothing, set `DEBUG = true` in
|
||||||
wrong.
|
`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
|
If Tampermonkey keeps serving a stale copy despite the update interval, open
|
||||||
the `file://` require with a local HTTP server, which does not cache:
|
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.
|
||||||
```js
|
|
||||||
// @require http://localhost:8777/rin-mobile.user.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Repository layout
|
### 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/
|
dev/
|
||||||
rin-mobile.dev.user.js loader, @require with absolute path
|
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
|
## 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:
|
mobile Ansicht umbaut. Es ersetzt das ursprüngliche Tabellenlayout vollständig:
|
||||||
der vorhandene Seiteninhalt wird ausgeblendet und ein neu gebautes DOM an
|
der vorhandene Seiteninhalt wird ausgeblendet und ein neu gebautes DOM an
|
||||||
`<body>` gehängt.
|
`<body>` gehängt.
|
||||||
@@ -230,89 +210,61 @@ anhand des Seitentyps:
|
|||||||
|
|
||||||
| Seite | Ansicht |
|
| 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 |
|
| `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 |
|
| `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 |
|
| alles andere | Unverändert — es werden keine Stile injiziert |
|
||||||
|
|
||||||
Eine `viewforum.php`-Seite ist nicht zwangsläufig eine Themenliste: eine
|
### Screenshots
|
||||||
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.
|
|
||||||
|
|
||||||
Foren, globale Ankündigungen, Ankündigungen, Stickies und Themen landen alle im
|
| Forenliste, die Abschnitte über den Themen zugeklappt | Thread, mit eingeblendeter Suchleiste |
|
||||||
selben Kartenstrom, womit die Trennung verlorengeht, die die Originalseite
|
|---|---|
|
||||||
zwischen ihnen zieht. Bei jedem Wechsel wird deshalb ein Gruppenkopf mit dem
|
| <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%"> |
|
||||||
Namen des Abschnitts und der Zahl seiner Karten eingezogen; Forenkarten tragen
|
| **Suchformular, oben: Schnellzugriffe und Forenwähler** | **Suchformular, unten: Optionen und Schaltflächen** |
|
||||||
zusätzlich eine farbige linke Kante. Die Beschriftung stammt aus der Trennzeile
|
| <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%"> |
|
||||||
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.
|
|
||||||
|
|
||||||
Ein Tipp auf den Kopf klappt seinen Abschnitt zu. Globale Ankündigungen,
|
### Was das Script liefert
|
||||||
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.
|
|
||||||
|
|
||||||
Beide Ansichten teilen sich dieselbe fixierte Navigationsleiste am unteren
|
Die Originalseite wird ausgeblendet und aus denselben Daten eine mobile Ansicht
|
||||||
Rand: erste / vorherige / nächste / letzte Seite und eine Positionsanzeige. Ein
|
gebaut, je eine pro Seitentyp:
|
||||||
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.
|
|
||||||
|
|
||||||
Beide Ansichten haben oben eine fixierte Brotkrumenleiste, übernommen aus dem
|
- **Listen als Karten.** Foren- und Themenzeilen werden zu Karten mit Titel,
|
||||||
`Board index » … » Main Forum`-Pfad des Forums. Ohne sie führt aus einem Thread
|
Beschreibung, Zahlen und letztem Beitrag, Kategoriezeilen zu
|
||||||
kein Weg zurück, weil die Originalseite ausgeblendet ist und phpBB das
|
Zwischenüberschriften.
|
||||||
Elternforum nirgends sonst verlinkt. Die Leiste lässt sich waagerecht schieben,
|
- **Gruppenköpfe.** Foren, Ankündigungen, Stickies und Themen trennt jeweils ein
|
||||||
wenn der Pfad breiter ist als der Bildschirm, und entfällt auf Seiten ohne
|
Kopf mit dem Namen des Abschnitts und der Zahl seiner Karten. Ankündigungen
|
||||||
Pfadangabe.
|
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 Beschriftungen des Scripts sind deutsch, alles aus dem Forum Übernommene
|
||||||
die das Script anfasst — nicht nur auf dem Login-Formular selbst. Der
|
behält seinen Wortlaut — `FOREN` im ersten Screenshot ist die Bezeichnung des
|
||||||
Sitzungszustand wird aus dem Menübalken der Originalseite gelesen: ein
|
Scripts für eine Gruppe, die die Seite selbst nicht beschriftet, `STICKIES` und
|
||||||
`mode=login`-Link bedeutet ausgeloggt, ein `mode=logout`-Link eingeloggt, die
|
`TOPICS` sind ihre eigenen Worte.
|
||||||
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.
|
|
||||||
|
|
||||||
Das Login-Formular selbst wird nicht neu gebaut: das Original-`<form>`-Element
|
Das Script läuft bei `document-start` und setzt ein mobiles Viewport-Meta-Tag.
|
||||||
wandert unverändert in das neue Layout und wird umgestylt, Tabellenzeilen
|
Ein `DEBUG`-Flag im Quelltext, standardmäßig aus, legt ein grünes
|
||||||
werden zu einer senkrechten Abfolge zusammengeklappt. Die Wiederverwendung
|
Diagnose-Overlay über die Seite: Version, gewählte Ansicht und Zahl der
|
||||||
statt Neubau erhält die versteckten Felder `sid` und `redirect`, die
|
umgewandelten Zeilen.
|
||||||
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.
|
|
||||||
|
|
||||||
### Installation unter iOS
|
### 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:
|
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/rin-mobile-userscript/rin-mobile.user.js
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
@@ -345,7 +297,7 @@ Projektordners muss die `@require`-Zeile in `dev/rin-mobile.dev.user.js` auf den
|
|||||||
eigenen Ablageort angepasst werden.
|
eigenen Ablageort angepasst werden.
|
||||||
|
|
||||||
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/rin-mobile-userscript/rin-mobile.user.js`.
|
||||||
Ein `@require`, das nicht aufgelöst werden kann, führt dazu, dass Tampermonkey
|
Ein `@require`, das nicht aufgelöst werden kann, führt dazu, dass Tampermonkey
|
||||||
das Script **komplett und ohne Fehlermeldung auf der Seite** überspringt. Das
|
das Script **komplett und ohne Fehlermeldung auf der Seite** überspringt. Das
|
||||||
sieht exakt so aus, als wäre gar kein Script installiert.
|
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
|
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.
|
||||||
|
|
||||||
|
#### 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
|
#### Geräte-Emulation
|
||||||
|
|
||||||
DevTools → *Device Toolbar* (`Strg+Umschalt+M`) → *Edit…* → eigenes Gerät mit
|
DevTools → *Device Toolbar* (`Strg+Umschalt+M`) → *Edit…* → eigenes Gerät mit
|
||||||
@@ -397,24 +374,27 @@ Windows mit (`npx playwright install webkit`).
|
|||||||
|
|
||||||
#### Fehlersuche
|
#### Fehlersuche
|
||||||
|
|
||||||
`dev/tm-test.user.js` installieren — das Script blendet nur einen roten Balken
|
Passiert überhaupt nichts, zuerst die beiden möglichen Ursachen trennen, bevor
|
||||||
ein, sonst nichts. Erscheint der Balken nicht, liegt das Problem bei
|
Code gelesen wird. Im Tampermonkey-Dashboard den Tab *Externals* des Loaders
|
||||||
Tampermonkey (siehe Einrichtung oben) und nicht am Script-Code. Erscheint er,
|
prüfen: ein leerer oder fehlgeschlagener Eintrag bedeutet, dass der
|
||||||
im Tampermonkey-Dashboard den Tab *Externals* des Loaders prüfen: ein leerer
|
`@require`-Pfad falsch ist. Ist der Eintrag da und trotzdem passiert nichts, in
|
||||||
oder fehlgeschlagener Eintrag bedeutet, dass der `@require`-Pfad falsch ist.
|
`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
|
Liefert Tampermonkey trotz Update-Intervall weiterhin einen veralteten Stand
|
||||||
aus, das `file://`-Require durch einen lokalen HTTP-Server ersetzen, der nicht
|
aus, im Tab *Externals* des Loaders den `@require`-Eintrag von Hand
|
||||||
zwischenspeichert:
|
aktualisieren oder den Loader einmal deaktivieren und wieder aktivieren —
|
||||||
|
beides verwirft die zwischengespeicherte Fassung.
|
||||||
```js
|
|
||||||
// @require http://localhost:8777/rin-mobile.user.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Aufbau des Repositories
|
### 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/
|
dev/
|
||||||
rin-mobile.dev.user.js Loader, @require mit absolutem Pfad
|
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
|
// @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 2.0
|
// @version 2.1
|
||||||
// @match *://cs.rin.ru/forum/*
|
// @match *://cs.rin.ru/forum/*
|
||||||
// @include http://localhost:*/*viewforum*
|
// @require file:///C:/Projekte/rin-mobile-userscript/rin-mobile.user.js
|
||||||
// @include http://127.0.0.1:*/*viewforum*
|
|
||||||
// @include http://localhost:*/*viewtopic*
|
|
||||||
// @include http://127.0.0.1:*/*viewtopic*
|
|
||||||
// @require file:///C:/Projekte/ios_safari_userscripts/rin-mobile.user.js
|
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// @grant none
|
// @grant none
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
// Der eigentliche Code steckt in der @require-Datei.
|
// Der eigentliche Code steckt in der @require-Datei.
|
||||||
// Aenderungen dort greifen nach einem Reload der Seite.
|
// 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==
|
// ==UserScript==
|
||||||
// @name RIN Mobile
|
// @name RIN Mobile
|
||||||
// @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom
|
// @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom
|
||||||
// @version 3.8
|
// @version 3.15
|
||||||
// @match *://cs.rin.ru/forum/*
|
// @match *://cs.rin.ru/forum/*
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
@@ -12,11 +12,36 @@
|
|||||||
var DEBUG = false;
|
var DEBUG = false;
|
||||||
var log = [];
|
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 h() { return document.head || document.documentElement; }
|
||||||
|
|
||||||
function txt(el) { return (el ? el.textContent || '' : '')
|
function txt(el) { return (el ? el.textContent || '' : '')
|
||||||
.replace(/\s+/g, ' ').trim(); }
|
.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
|
// Der Seitentyp entscheidet, welche Ansicht gebaut wird. Beide Ansichten
|
||||||
// liegen in einem Script, weil sie sich Grundstile und Klassennamen teilen.
|
// liegen in einem Script, weil sie sich Grundstile und Klassennamen teilen.
|
||||||
// Als getrennte Scripts waren auf Threadseiten beide Stylesheets aktiv und
|
// Als getrennte Scripts waren auf Threadseiten beide Stylesheets aktiv und
|
||||||
@@ -35,6 +60,12 @@
|
|||||||
: '';
|
: '';
|
||||||
log.push('ansicht=' + (view || 'keine'));
|
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]')) {
|
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';
|
||||||
@@ -86,10 +117,21 @@
|
|||||||
color:#eee !important; text-decoration:none !important; }
|
color:#eee !important; text-decoration:none !important; }
|
||||||
.rm-crumbs a:last-of-type { font-weight:650; }
|
.rm-crumbs a:last-of-type { font-weight:650; }
|
||||||
.rm-crumbs .rm-sep { flex:none; font-size:13px; color:#eee; opacity:.45; }
|
.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;
|
font-size:13px !important; border-radius:99px;
|
||||||
background:rgba(128,128,128,.18);
|
background:rgba(128,128,128,.18);
|
||||||
border:1px solid rgba(160,160,160,.4); }
|
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
|
/* Nur gesetzt, wenn die Leiste wirklich gebaut wurde — ohne Brotkrumen
|
||||||
im Original-DOM bleibt der Platz sonst leer reserviert. */
|
im Original-DOM bleibt der Platz sonst leer reserviert. */
|
||||||
body.rm-crumbed .rm-root {
|
body.rm-crumbed .rm-root {
|
||||||
@@ -97,6 +139,38 @@
|
|||||||
body.rm-crumbed .rm-dbg {
|
body.rm-crumbed .rm-dbg {
|
||||||
margin-top:calc(45px + env(safe-area-inset-top)); }
|
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 === */
|
/* === Themen- und Forenliste === */
|
||||||
|
|
||||||
.rm-sec { padding:18px 14px 6px; font-size:12px; font-weight:700;
|
.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;
|
font-size:21px; font-weight:650; line-height:1.3;
|
||||||
overflow-wrap:break-word; }
|
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;
|
.rm-post { box-sizing:border-box !important;
|
||||||
width:100% !important; max-width:100% !important;
|
width:100% !important; max-width:100% !important;
|
||||||
padding:14px !important;
|
padding:14px !important;
|
||||||
|
scroll-margin-top:calc(53px + env(safe-area-inset-top));
|
||||||
border-top:1px solid rgba(128,128,128,.25) !important; }
|
border-top:1px solid rgba(128,128,128,.25) !important; }
|
||||||
|
|
||||||
.rm-head { display:flex; align-items:center; gap:9px;
|
.rm-head { display:flex; align-items:center; gap:9px;
|
||||||
@@ -258,10 +336,104 @@
|
|||||||
text-decoration:none !important; color:inherit !important; }
|
text-decoration:none !important; color:inherit !important; }
|
||||||
.rm-login form input[type="checkbox"] { width:18px; height:18px;
|
.rm-login form input[type="checkbox"] { width:18px; height:18px;
|
||||||
vertical-align:middle; margin-right:8px; }
|
vertical-align:middle; margin-right:8px; }
|
||||||
.rm-login form input[type="submit"] {
|
.rm-login form button[type="submit"] {
|
||||||
width:100%; font-size:16px; font-weight:650; padding:13px;
|
-webkit-appearance:none; appearance:none;
|
||||||
margin-top:20px; border-radius:10px; color:#fff; border:0;
|
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); }
|
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 === */
|
/* === Seitennavigation, beide Ansichten === */
|
||||||
|
|
||||||
@@ -388,11 +560,31 @@
|
|||||||
if (!n) return;
|
if (!n) return;
|
||||||
|
|
||||||
var st = loginState();
|
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');
|
var auth = document.createElement('a');
|
||||||
auth.className = 'rm-auth';
|
auth.className = 'rm-auth';
|
||||||
auth.href = st.href;
|
auth.href = st.href;
|
||||||
auth.textContent = st.in ? (st.name || 'Logout') : 'Login';
|
auth.textContent = st.in ? (st.name || 'Logout') : 'Login';
|
||||||
bar.appendChild(auth);
|
group.appendChild(auth);
|
||||||
|
bar.appendChild(group);
|
||||||
|
|
||||||
document.body.appendChild(bar);
|
document.body.appendChild(bar);
|
||||||
document.body.classList.add('rm-crumbed');
|
document.body.classList.add('rm-crumbed');
|
||||||
@@ -946,7 +1138,199 @@
|
|||||||
return n;
|
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() {
|
function buildList() {
|
||||||
|
if (buildSearchForm()) return;
|
||||||
|
if (buildSearchResults()) return;
|
||||||
|
|
||||||
// Dokumentreihenfolge entscheidet: in einer Forenzeile steht der Forenname
|
// Dokumentreihenfolge entscheidet: in einer Forenzeile steht der Forenname
|
||||||
// vor dem Link auf den letzten Beitrag. Ohne diese Reihenfolge faellt die
|
// vor dem Link auf den letzten Beitrag. Ohne diese Reihenfolge faellt die
|
||||||
// Zeile als Thema an, der Forenname verschwindet und nur die Beschreibung
|
// Zeile als Thema an, der Forenname verschwindet und nur die Beschreibung
|
||||||
@@ -1032,6 +1416,47 @@
|
|||||||
// Thread
|
// 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')
|
// Der Beitragstext liegt in einer verschachtelten Tabelle. closest('tr')
|
||||||
// trifft deshalb die innere Zeile, der Autor- und Profilspalte fehlen.
|
// trifft deshalb die innere Zeile, der Autor- und Profilspalte fehlen.
|
||||||
// Aufsteigen, bis die Zeile eine Profil- oder Autorenspalte enthaelt.
|
// Aufsteigen, bis die Zeile eine Profil- oder Autorenspalte enthaelt.
|
||||||
@@ -1087,6 +1512,16 @@
|
|||||||
pr = pr.previousElementSibling;
|
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 ---
|
// --- Username ---
|
||||||
var user = '';
|
var user = '';
|
||||||
for (var i = 0; i < cells.length && !user; i++) {
|
for (var i = 0; i < cells.length && !user; i++) {
|
||||||
@@ -1234,6 +1669,8 @@
|
|||||||
if (!td.children.length && !txt(td)) td.classList.add('rm-hidden');
|
if (!td.children.length && !txt(td)) td.classList.add('rm-hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
inputToButton(form.querySelector('input[type="submit"]'));
|
||||||
|
|
||||||
root.appendChild(form);
|
root.appendChild(form);
|
||||||
replaceBody(root);
|
replaceBody(root);
|
||||||
addCrumbs();
|
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() {
|
function run() {
|
||||||
try {
|
try {
|
||||||
if (view === 'liste') buildList();
|
if (view === 'liste') buildList();
|
||||||
@@ -1284,10 +1761,13 @@
|
|||||||
else if (view === 'login') buildLogin();
|
else if (view === 'login') buildLogin();
|
||||||
} catch (e) { log.push('build: ' + e.message); }
|
} catch (e) { log.push('build: ' + e.message); }
|
||||||
|
|
||||||
|
if (view && document.querySelector('.rm-root')) holdStart();
|
||||||
|
|
||||||
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 v3.8\n' + log.join('\n');
|
d.textContent = 'RIN Mobile' + (VERSION ? ' v' + VERSION : '') +
|
||||||
|
'\n' + log.join('\n');
|
||||||
document.body.insertBefore(d, document.body.firstChild);
|
document.body.insertBefore(d, document.body.firstChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user