fix: recognise forum rows in the list view

A viewforum page is not always a list of topics — a category page lists
forums, and a forum page can carry both. Every row was treated as a topic,
so on those pages the only link the row offered was the icon in its last-post
column: the forum name was dropped, its description ended up in the muted
footer line, and nothing was left to tap. Moving between forums on a phone
was impossible.

Rows are now classified by their first usable link, which relies on document
order: a forum row names the forum before it links the last post. Detection
prefers phpBB's own classes and falls back to the shape of the row, because
the board runs a custom theme whose markup could not be verified — the page
requires a login.

That fallback needs a brake. The page scaffolding is itself a table, so a
link outside any list row resolves to a row containing the whole document,
which passed for a forum row and turned the page header, a stylesheet and
inline scripts into a forum description. Rows carrying nested tables, forms
or scripts are therefore rejected, and descriptions are read only from the
title cell's own text.
This commit is contained in:
Andre
2026-08-02 14:55:39 +02:00
parent bc9c4937ff
commit 3609d9e151
3 changed files with 268 additions and 65 deletions
+30
View File
@@ -56,6 +56,28 @@ Die Funktion markiert *alle* vorhandenen `body`-Kinder mit `.rm-hidden`. Das
Sheet wird deshalb erst beim ersten Antippen gebaut — das loest das Problem Sheet wird deshalb erst beim ersten Antippen gebaut — das loest das Problem
nebenbei mit. nebenbei mit.
**In der Liste entscheidet die Dokumentreihenfolge ueber den Zeilentyp.**
`buildList()` sammelt Links auf `viewforum.php` und `viewtopic.php` in einem
Rutsch ein und ordnet jede `tr` nach ihrem ersten verwertbaren Link zu. Eine
Forenzeile verlinkt in der Spalte „letzter Beitrag" ebenfalls `viewtopic.php` —
zaehlte man Themenlinks zuerst, faellt die Zeile als Thema an, der Forenname
verschwindet und nur die Beschreibung bleibt als `.rm-last` uebrig. Genau so
war es vor 3.2. Brotkrumen und „Mark forums read" verlinken ebenfalls Foren,
stehen aber in Zeilen ohne Forenmerkmale und werden verworfen.
**Das Seitengeruest ist selbst eine Tabelle.** `closest('tr')` liefert fuer
einen Link ausserhalb einer Listenzeile die Layoutzeile, die die ganze Seite
enthaelt. Ohne Bremse wurde daraus eine Karte mit dem kompletten Seitentext —
Kopfzeile, Stylesheet und Inline-Scripts inklusive — als Forenbeschreibung.
Zeilen mit `table`, `script`, `style` oder `form` darin sind deshalb keine
Kandidaten, und die Beschreibung wird nur aus den direkten Textknoten der
Titelzelle gebildet, nie aus deren gesamtem Text.
**Das Theme ist `rinDark`, nicht subsilver2.** Die Klassennamen aus der
phpBB-Dokumentation (`forumlink`, `forumdesc`) sind fuer die Forenliste
deshalb nur eine Vermutung, solange kein `dev/viewforum.php` vorliegt. Die
Erkennung faellt bewusst auf die Form der Zeile zurueck.
**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`
-Leiste, in dem nie gescrollt wird — sie klebte nie. Fixierte Nachfahren -Leiste, in dem nie gescrollt wird — sie klebte nie. Fixierte Nachfahren
@@ -110,6 +132,14 @@ Fuer die Weiche und die Listenansicht genuegt eine handgeschriebene Tabelle mit
Weiche wie `posting.php`: dort darf weder eine Ansicht gebaut noch ein Weiche wie `posting.php`: dort darf weder eine Ansicht gebaut noch ein
`<style>` injiziert werden. `<style>` injiziert werden.
Fuer die Forenliste braucht es eine zweite Tabelle im subsilver2-Schnitt:
`td.cat > h4 > a` als Kategoriezeile, darunter Zeilen mit `a.forumlink`, drei
`p.forumdesc` (Beschreibung, Moderatoren, Unterforen), zwei Zahlenspalten und
einer Spalte „letzter Beitrag" mit Link auf `viewtopic.php`. Vier Faelle sind
sinnvoll: reine Forenliste, reine Themenliste, beides auf einer Seite, und
dieselbe Forenliste ohne die Klassen `forumlink`/`forumdesc` — die muss ueber
die Zahlenspalten dasselbe Ergebnis liefern.
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
+19 -2
View File
@@ -15,10 +15,18 @@ page type:
| Page | View | | Page | View |
|---|---| |---|---|
| `viewforum.php`, `search.php`, forum root | Topic list as cards | | `viewforum.php`, `search.php`, forum root | Topic and forum list 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 |
| 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
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.
The thread navigation bar holds first / previous / next / last and a page The thread navigation bar holds first / previous / next / last and a page
indicator. Tapping the indicator opens a sheet with a number field and a grid of indicator. Tapping the indicator opens a sheet with a number field and a grid of
all pages. Targets are computed as `start = (page - 1) × per_page` rather than all pages. Targets are computed as `start = (page - 1) × per_page` rather than
@@ -178,10 +186,19 @@ anhand des Seitentyps:
| Seite | Ansicht | | Seite | Ansicht |
|---|---| |---|---|
| `viewforum.php`, `search.php`, Forumwurzel | Themenliste als Karten | | `viewforum.php`, `search.php`, Forumwurzel | Themen- und Forenliste als Karten |
| `viewtopic.php` | Thread als Beitragsstrom mit fixierter Navigationsleiste | | `viewtopic.php` | Thread als Beitragsstrom mit fixierter Navigationsleiste |
| 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
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.
Die Navigationsleiste im Thread enthält erste / vorherige / nächste / letzte Die Navigationsleiste im Thread enthält erste / vorherige / nächste / letzte
Seite und eine Positionsanzeige. Ein Tipp darauf öffnet ein Sheet mit Seite und eine Positionsanzeige. Ein Tipp darauf öffnet ein Sheet mit
Zahlenfeld und einem Raster aller Seiten. Die Ziele werden als Zahlenfeld und einem Raster aller Seiten. Die Ziele werden als
+219 -63
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.1 // @version 3.2
// @match *://cs.rin.ru/forum/* // @match *://cs.rin.ru/forum/*
// @run-at document-start // @run-at document-start
// ==/UserScript== // ==/UserScript==
@@ -86,7 +86,17 @@
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)); }
/* === Themenliste === */ /* === Themen- und Forenliste === */
.rm-sec { padding:18px 14px 6px; font-size:12px; font-weight:700;
text-transform:uppercase; letter-spacing:.06em; opacity:.55; }
.rm-sec a { text-decoration:none !important; color:inherit !important; }
.rm-desc { margin-top:6px; font-size:14px; opacity:.72;
overflow-wrap:break-word; }
.rm-subs { margin-top:9px; display:flex; flex-wrap:wrap; gap:6px; }
.rm-subs a { font-size:12px; padding:4px 9px; border-radius:99px;
border:1px solid rgba(128,128,128,.4);
text-decoration:none !important; color:inherit !important; }
.rm-card { box-sizing:border-box !important; .rm-card { box-sizing:border-box !important;
display:block !important; display:block !important;
@@ -284,14 +294,67 @@
} }
// ========================================================================== // ==========================================================================
// Themenliste // Themenliste und Forenliste
// ========================================================================== // ==========================================================================
function breakable(s) { function breakable(s) {
return s.replace(/([|\/\\,;:>\]\)])/g, '$1\u200B'); return s.replace(/([|\/\\,;:>\]\)])/g, '$1\u200B');
} }
function convert(tr) { // Karten teilen sich Titel, Zahlenzeile und Fusszeile; nur die Quellen
// dafuer unterscheiden sich zwischen Thema und Forum.
function card(titleA, badgeable) {
var el = document.createElement('div');
el.className = 'rm-card';
var main = document.createElement('a');
main.className = 'rm-main';
main.href = titleA.href;
var raw = txt(titleA);
var badge = badgeable ? (raw.match(/^\[([^\]]{1,20})\]/) || [])[1] : null;
if (badge) {
var b = document.createElement('span');
b.className = 'rm-badge';
b.textContent = badge;
main.appendChild(b);
raw = raw.replace(/^\[[^\]]{1,20}\]\s*/, '');
}
main.appendChild(document.createTextNode(breakable(raw)));
el.appendChild(main);
return el;
}
function metaRow(parts) {
var meta = document.createElement('div');
meta.className = 'rm-meta';
parts.forEach(function (s) {
if (!s) return;
var e = document.createElement('span');
e.textContent = s;
meta.appendChild(e);
});
return meta.children.length ? meta : null;
}
function footRow(last, jump, label) {
var foot = document.createElement('div');
foot.className = 'rm-foot';
var l = document.createElement('div');
l.className = 'rm-last';
l.textContent = last;
foot.appendChild(l);
if (jump) {
var j = document.createElement('a');
j.className = 'rm-jump';
j.href = jump.href;
j.textContent = label;
foot.appendChild(j);
}
return foot;
}
function convertTopic(tr) {
var titleA = tr.querySelector('a.topictitle') || var titleA = tr.querySelector('a.topictitle') ||
tr.querySelector('a[href*="viewtopic.php"]'); tr.querySelector('a[href*="viewtopic.php"]');
if (!titleA) return null; if (!titleA) return null;
@@ -312,80 +375,173 @@
} }
}); });
var card = document.createElement('div'); var el = card(titleA, true);
card.className = 'rm-card'; var meta = metaRow([author,
nums[0] ? nums[0] + ' Antworten' : '',
var main = document.createElement('a'); nums[1] ? nums[1] + ' Views' : '']);
main.className = 'rm-main'; if (meta) el.appendChild(meta);
main.href = titleA.href; el.appendChild(footRow(last, jump, 'Neueste ↓'));
return el;
var raw = txt(titleA);
var badge = (raw.match(/^\[([^\]]{1,20})\]/) || [])[1];
if (badge) {
var b = document.createElement('span');
b.className = 'rm-badge';
b.textContent = badge;
main.appendChild(b);
raw = raw.replace(/^\[[^\]]{1,20}\]\s*/, '');
} }
main.appendChild(document.createTextNode(breakable(raw)));
card.appendChild(main);
var meta = document.createElement('div'); // Forenzeilen tragen ihren Namen in a.forumlink, die Beschreibung und die
meta.className = 'rm-meta'; // Unterforen stehen als weitere Absaetze in derselben Zelle.
function add(s) { function convertForum(tr) {
var e = document.createElement('span'); var titleA = forumTitle(tr);
e.textContent = s; if (!titleA) return null;
meta.appendChild(e);
var titleCell = titleA.closest('td');
var nums = [], last = '', jump = null;
Array.prototype.forEach.call(tr.children, function (td) {
if (td === titleCell) return;
var t = txt(td);
if (/^\d[\d.,]*$/.test(t)) { nums.push(t); return; }
if (t) {
last = t;
jump = td.querySelector('a[href*="viewtopic.php"]');
} }
if (author) add(author); });
if (nums[0]) add(nums[0] + ' Antworten');
if (nums[1]) add(nums[1] + ' Views');
if (meta.children.length) card.appendChild(meta);
var foot = document.createElement('div'); var el = card(titleA, false);
foot.className = 'rm-foot';
var l = document.createElement('div'); // Beschreibung, Moderatoren und Unterforen benutzen denselben Absatztyp.
l.className = 'rm-last'; // Die beiden letzten sind nur an ihrer Einleitung zu erkennen.
l.textContent = last; var desc = '';
foot.appendChild(l); if (titleCell) {
if (jump) { titleCell.querySelectorAll('p.forumdesc, span.forumdesc')
var j = document.createElement('a'); .forEach(function (p) {
j.className = 'rm-jump'; var t = txt(p);
j.href = jump.href; if (desc || !t || /^(Moderator|Subforum|Unterforum)/i.test(t)) return;
j.textContent = 'Neueste ↓'; desc = t;
foot.appendChild(j); });
}
// Ohne die Klasse steht die Beschreibung als blosser Text neben dem Namen.
// Nur direkte Textknoten der Zelle: der Text der ganzen Zelle einzusammeln
// holt bei einer verkannten Zeile den halben Seiteninhalt mit.
if (!desc && titleCell) {
var loose = '';
Array.prototype.forEach.call(titleCell.childNodes, function (n) {
if (n.nodeType === 3) loose += ' ' + n.nodeValue;
});
desc = loose.replace(/\s+/g, ' ').trim()
.split(/Moderators?:|Subforums?:|Unterforen?:/)[0].trim();
if (desc.length > 300) desc = '';
}
if (desc) {
var d = document.createElement('div');
d.className = 'rm-desc';
d.textContent = desc;
el.appendChild(d);
} }
card.appendChild(foot);
return card; // Unterforen sind sonst nur ueber den Umweg des Elternforums erreichbar.
var subs = document.createElement('div');
subs.className = 'rm-subs';
if (titleCell) {
titleCell.querySelectorAll('a[href*="viewforum.php"]')
.forEach(function (a) {
if (a === titleA || !txt(a)) return;
var s = document.createElement('a');
s.href = a.href;
s.textContent = txt(a);
subs.appendChild(s);
});
}
if (subs.children.length) el.appendChild(subs);
var meta = metaRow([nums[0] ? nums[0] + ' Themen' : '',
nums[1] ? nums[1] + ' Beiträge' : '']);
if (meta) el.appendChild(meta);
if (last || jump) el.appendChild(footRow(last, jump, 'Letzter ↓'));
return el;
}
function forumTitle(tr) {
var a = tr.querySelector('a.forumlink');
if (a) return a;
// Ohne die Klasse bleibt der erste Link auf ein Forum, der Text traegt und
// kein Unterforum ist.
var found = null;
tr.querySelectorAll('a[href*="viewforum.php"]').forEach(function (x) {
if (found || !txt(x) || x.classList.contains('subforum') ||
x.classList.contains('breadcrumbs') || x.closest('p.breadcrumbs')) return;
found = x;
});
return found;
}
function section(a) {
var el = document.createElement('div');
el.className = 'rm-sec';
var l = document.createElement('a');
l.href = a.href;
l.textContent = txt(a);
el.appendChild(l);
return el;
}
// Welche Karte eine Zeile ergibt, entscheidet der erste verwertbare Link
// darin. Beiwerk wie die Brotkrumen oder "Mark topics read" verlinkt
// ebenfalls auf viewforum.php, steht aber in keiner Zeile, die eine Karte
// hergibt — deshalb muss eine Forenzeile als solche erkennbar sein.
function rowKind(a, tr) {
if (!txt(a) || a.classList.contains('breadcrumbs') ||
a.closest('p.breadcrumbs')) return '';
if (/viewtopic\.php/.test(a.getAttribute('href') || ''))
return tr.querySelector('a.forumlink') ? 'forum' : 'thema';
// Das Seitengeruest besteht bei phpBB selbst aus Tabellen. Enthaelt eine
// Zeile eine weitere Tabelle oder ein Script, ist sie Layout und keine
// Forenzeile — ohne diese Bremse wird die halbe Seite zu einer Karte.
if (tr.querySelector('table, script, style, form')) return '';
if (a.closest('td.cat') || a.closest('h4')) return 'sektion';
if (a.classList.contains('forumlink')) return 'forum';
if (a.classList.contains('subforum')) return '';
// Fehlen die phpBB-Klassen, bleibt die Form der Zeile: zwei rein
// numerische Spalten hat nur eine Forenzeile mit Themen- und Beitragszahl.
return tr.querySelector('.forumdesc') || numCells(tr) >= 2 ? 'forum' : '';
}
function numCells(tr) {
var n = 0;
Array.prototype.forEach.call(tr.children, function (td) {
if (/^\d[\d.,]*$/.test(txt(td))) n++;
});
return n;
} }
function buildList() { function buildList() {
// Dokumentreihenfolge entscheidet: in einer Forenzeile steht der Forenname
// vor dem Link auf den letzten Beitrag. Ohne diese Reihenfolge faellt die
// Zeile als Thema an, der Forenname verschwindet und nur die Beschreibung
// bleibt uebrig.
var links = document.querySelectorAll( var links = document.querySelectorAll(
'a.topictitle,a[href*="viewtopic.php"]'); 'a.forumlink,a.topictitle,a[href*="viewforum.php"],a[href*="viewtopic.php"]');
log.push('themenlinks=' + links.length);
if (!links.length) return; var rows = [];
links.forEach(function (a) {
var tr = a.closest('tr');
if (!tr || tr.dataset.rm || !tr.closest('table')) return;
var kind = rowKind(a, tr);
if (!kind) return;
tr.dataset.rm = '1';
rows.push({ tr: tr, a: a, kind: kind });
});
log.push('zeilen=' + rows.length);
if (!rows.length) return;
var root = document.createElement('div'); var root = document.createElement('div');
root.className = 'rm-root'; root.className = 'rm-root';
var rows = [], tables = new Set();
links.forEach(function (a) {
var tr = a.closest('tr');
if (!tr || tr.dataset.rm) return;
var tbl = tr.closest('table');
if (!tbl) return;
tr.dataset.rm = '1';
rows.push(tr);
tables.add(tbl);
});
var built = 0; var built = 0;
rows.forEach(function (tr) { rows.forEach(function (r) {
try { try {
var c = convert(tr); var c = r.kind === 'thema' ? convertTopic(r.tr)
if (c) { root.appendChild(c); built++; } : r.kind === 'forum' ? convertForum(r.tr)
: section(r.a);
if (!c) return;
root.appendChild(c);
if (r.kind !== 'sektion') built++;
} catch (e) { log.push('convert: ' + e.message); } } catch (e) { log.push('convert: ' + e.message); }
}); });
log.push('karten=' + built); log.push('karten=' + built);
@@ -790,7 +946,7 @@
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.1\n' + log.join('\n'); d.textContent = 'RIN Mobile v3.2\n' + log.join('\n');
document.body.insertBefore(d, document.body.firstChild); document.body.insertBefore(d, document.body.firstChild);
} }
} }