6 Commits
Author SHA1 Message Date
Andre e459973149 fix: start threads at the top on iOS Safari
The mobile view is assembled after DOMContentLoaded, long after the browser
has settled on a scroll position, and the rebuild never touched it. A rebuilt
thread stays taller than the viewport, so an inherited offset was never
clamped back to zero and the thread opened somewhere inside its first post.
Safari makes this visible where desktop Chrome does not: it restores the
position of an address it has seen before and revises it again after load and
after late images settle, so resetting once is not enough.

Deciding what "top" means also required a target for addresses that carry a
fragment. Post anchors existed only in the original markup, which the rebuild
hides, so a jump from the topic list had nowhere to land and ended up wherever
the browser happened to stand.
2026-08-03 11:33:14 +02:00
Andre 2ea94f8a2a docs: point loader path at the renamed project folder
The project directory no longer carries the name it had when the loader was
written. A @require that does not resolve makes Tampermonkey skip the script
silently, which is indistinguishable from it never having been installed —
so a stale path here costs a debugging session before anyone suspects the
path. The README carried the same path in its setup instructions, and its
title still described a collection of scripts rather than the single one
that remains.
2026-08-03 10:33:27 +02:00
Andre c477537826 docs: lead with screenshots and correct outdated documentation
Both documents had drifted away from the repository they describe: they
pointed at fixtures and a helper script that are not present, documented a
local-server workflow that is no longer used, and filed one page type under
the wrong view. Following them meant setting up things that cannot work while
trusting claims that no longer held.

The README also explained every design decision before showing a single
result, so the question a reader asks first — what does this actually look
like — took the longest to answer. Screenshots and a short feature list now
carry that job. The reasoning stays where it is needed, next to the code.

Restricting the dev loader to the live site closes a side effect that only
surfaced locally: its patterns matched page names common enough that the
stylesheet would have leaked into unrelated local projects.

The version number existed in two places and had already drifted apart.
Reading it back from the metadata block leaves one place to change it.
2026-08-03 10:21:05 +02:00
Andre e7f3f56456 feat: default search form to Main Forum, subforums off
The server renders search.php with no forum preselected and "Search
subforums" set to Yes, which searches the whole board by default. Most
searches from the mobile view are scoped to Main Forum only, so leaving
the server defaults in place meant re-selecting the same options on
every visit.
2026-08-03 09:11:48 +02:00
Andre a8ca211805 feat: optimize search.php for the mobile view
search.php had no mobile treatment: the query form rendered as an
unstyled desktop table, and a submitted search fell back to the regular
forum-listing view on the untested assumption that results looked like
topic rows.

The query form is now recognised by its forum-picker select, the one
element the small "search this forum/topic" widgets embedded on other
pages don't share — they reuse the same form action and keyword field
name and would otherwise be picked up by mistake. Results in the "Posts"
display mode the in-thread search bar produces turned out to carry post
excerpts, not topic rows, and needed their own card layout instead of
running through the list view, which would have misread the topic and
profile links inside each hit.

Submit/reset controls kept native iOS Safari chrome even under
-webkit-appearance:none once restyled, clipping their labels — only
visible on device, not in the emulator this project is developed
against. Replacing them with real <button> elements (same name/value,
so the submitted form data is unchanged) fixed it where the CSS-only
attempt didn't; touch targets were sized up to match.
2026-08-03 09:03:27 +02:00
Andre 21820b88d0 feat: add search access to the crumbs bar for logged-in users
The mobile view hides the whole original body behind replaceBody(),
which removed every path back to phpBB's search: the header icon and,
inside a thread, the per-topic "Search this topic..." form. Without
this there was no way to search at all from the rebuilt view.

The crumbs bar has no room for a third element next to the breadcrumb
trail and the login pill, so the new search button only shows once a
session exists and shares the pill's existing right-aligned group
instead of claiming separate space. On viewtopic it reveals a second
bar that reuses the original per-topic search form in place, keeping
its hidden topic-id and scope fields intact rather than reconstructing
them and risking a silently broken query.
2026-08-03 08:02:14 +02:00
8 changed files with 888 additions and 222 deletions
+229 -24
View File
@@ -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
+165 -185
View File
@@ -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
``` ```
+7 -6
View File
@@ -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
View File
@@ -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);
} }
} }