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
+219 -63
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name RIN Mobile
// @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom
// @version 3.1
// @version 3.2
// @match *://cs.rin.ru/forum/*
// @run-at document-start
// ==/UserScript==
@@ -86,7 +86,17 @@
body.rm-crumbed .rm-dbg {
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;
display:block !important;
@@ -284,14 +294,67 @@
}
// ==========================================================================
// Themenliste
// Themenliste und Forenliste
// ==========================================================================
function breakable(s) {
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') ||
tr.querySelector('a[href*="viewtopic.php"]');
if (!titleA) return null;
@@ -312,80 +375,173 @@
}
});
var card = document.createElement('div');
card.className = 'rm-card';
var el = card(titleA, true);
var meta = metaRow([author,
nums[0] ? nums[0] + ' Antworten' : '',
nums[1] ? nums[1] + ' Views' : '']);
if (meta) el.appendChild(meta);
el.appendChild(footRow(last, jump, 'Neueste ↓'));
return el;
}
var main = document.createElement('a');
main.className = 'rm-main';
main.href = titleA.href;
// Forenzeilen tragen ihren Namen in a.forumlink, die Beschreibung und die
// Unterforen stehen als weitere Absaetze in derselben Zelle.
function convertForum(tr) {
var titleA = forumTitle(tr);
if (!titleA) return null;
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*/, '');
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"]');
}
});
var el = card(titleA, false);
// Beschreibung, Moderatoren und Unterforen benutzen denselben Absatztyp.
// Die beiden letzten sind nur an ihrer Einleitung zu erkennen.
var desc = '';
if (titleCell) {
titleCell.querySelectorAll('p.forumdesc, span.forumdesc')
.forEach(function (p) {
var t = txt(p);
if (desc || !t || /^(Moderator|Subforum|Unterforum)/i.test(t)) return;
desc = t;
});
}
main.appendChild(document.createTextNode(breakable(raw)));
card.appendChild(main);
var meta = document.createElement('div');
meta.className = 'rm-meta';
function add(s) {
var e = document.createElement('span');
e.textContent = s;
meta.appendChild(e);
// 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 (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');
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 = 'Neueste ↓';
foot.appendChild(j);
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() {
// 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(
'a.topictitle,a[href*="viewtopic.php"]');
log.push('themenlinks=' + links.length);
if (!links.length) return;
'a.forumlink,a.topictitle,a[href*="viewforum.php"],a[href*="viewtopic.php"]');
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');
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;
rows.forEach(function (tr) {
rows.forEach(function (r) {
try {
var c = convert(tr);
if (c) { root.appendChild(c); built++; }
var c = r.kind === 'thema' ? convertTopic(r.tr)
: 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); }
});
log.push('karten=' + built);
@@ -790,7 +946,7 @@
if (DEBUG) {
var d = document.createElement('div');
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);
}
}