mirror of
https://github.com/Andre-Bzk/rin-mobile-userscript.git
synced 2026-11-15 09:33:08 +01:00
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.
1399 lines
56 KiB
JavaScript
1399 lines
56 KiB
JavaScript
// ==UserScript==
|
||
// @name RIN Mobile
|
||
// @description Mobile Ansicht fuer cs.rin.ru: Themenlisten als Karten, Threads als Beitragsstrom
|
||
// @version 3.9
|
||
// @match *://cs.rin.ru/forum/*
|
||
// @run-at document-start
|
||
// ==/UserScript==
|
||
|
||
(function () {
|
||
'use strict';
|
||
|
||
var DEBUG = false;
|
||
var log = [];
|
||
|
||
function h() { return document.head || document.documentElement; }
|
||
|
||
function txt(el) { return (el ? el.textContent || '' : '')
|
||
.replace(/\s+/g, ' ').trim(); }
|
||
|
||
// Der Seitentyp entscheidet, welche Ansicht gebaut wird. Beide Ansichten
|
||
// liegen in einem Script, weil sie sich Grundstile und Klassennamen teilen.
|
||
// Als getrennte Scripts waren auf Threadseiten beide Stylesheets aktiv und
|
||
// die Ladereihenfolge entschied bei gleich benannten Regeln ueber das
|
||
// Layout.
|
||
var here = location.pathname + location.search;
|
||
// index.php wird ueber den Pfad geprueft, nicht ueber "here": der
|
||
// Login-Redirect haengt "index.php" gelegentlich als Wert eines
|
||
// Query-Parameters an ucp.php, ueber "here" faellt das faelschlich hier
|
||
// hinein statt in den Login-Zweig.
|
||
var view = /viewtopic\.php/.test(here) ? 'thread'
|
||
: /viewforum\.php|search\.php/.test(here) ||
|
||
/\/index\.php$/.test(location.pathname) ||
|
||
location.pathname === '/forum/' ? 'liste'
|
||
: /ucp\.php/.test(here) && /mode=login/.test(here) ? 'login'
|
||
: '';
|
||
log.push('ansicht=' + (view || 'keine'));
|
||
|
||
if (view && !document.querySelector('meta[name="viewport"][data-rm]')) {
|
||
var m = document.createElement('meta');
|
||
m.name = 'viewport';
|
||
m.content = 'width=device-width, initial-scale=1, viewport-fit=cover';
|
||
m.setAttribute('data-rm', '1');
|
||
h().appendChild(m);
|
||
}
|
||
|
||
if (view) {
|
||
var st = document.createElement('style');
|
||
st.textContent = `
|
||
/* === Grundlage, alle Ansichten === */
|
||
|
||
html, body { max-width:100% !important; overflow-x:hidden !important;
|
||
margin:0 !important; padding:0 !important; }
|
||
body { font-size:16px !important;
|
||
-webkit-text-size-adjust:100% !important; }
|
||
img { max-width:100% !important; height:auto !important; }
|
||
|
||
.rm-dbg { background:#222; color:#0f0; font:12px/1.45 monospace;
|
||
padding:8px 10px; white-space:pre-wrap;
|
||
border-bottom:2px solid #0f0; }
|
||
.rm-hidden { display:none !important; }
|
||
|
||
.rm-root { box-sizing:border-box !important; display:block !important;
|
||
width:100vw !important; max-width:100vw !important;
|
||
margin:0 !important; padding:0 !important;
|
||
overflow:hidden !important; }
|
||
/* Freihaltung hinter der fixierten Seitenleiste. Die Klasse setzt die
|
||
Leiste selbst, damit ohne sie kein leerer Streifen bleibt. */
|
||
.rm-root.rm-paged { padding-bottom:64px !important; }
|
||
|
||
/* Brotkrumen, alle Ansichten. Fixiert statt sticky: .rm-root traegt
|
||
overflow:hidden und wuerde als Scrollport einer sticky-Leiste dienen,
|
||
die darin nie klebt. Fixierte Nachfahren klammert es nicht, weil der
|
||
Root keinen eigenen Containing Block aufspannt. */
|
||
.rm-crumbs { box-sizing:border-box;
|
||
position:fixed; top:0; left:0; right:0; z-index:9998;
|
||
display:flex; align-items:center; gap:6px;
|
||
height:calc(44px + env(safe-area-inset-top));
|
||
padding:env(safe-area-inset-top) 12px 0;
|
||
overflow-x:auto; overflow-y:hidden;
|
||
-webkit-overflow-scrolling:touch; white-space:nowrap;
|
||
background:rgba(28,28,30,.94);
|
||
-webkit-backdrop-filter:blur(12px);
|
||
backdrop-filter:blur(12px);
|
||
border-bottom:1px solid rgba(128,128,128,.3); }
|
||
.rm-crumbs a { flex:none; padding:8px 2px; font-size:14px;
|
||
color:#eee !important; text-decoration:none !important; }
|
||
.rm-crumbs a:last-of-type { font-weight:650; }
|
||
.rm-crumbs .rm-sep { flex:none; font-size:13px; color:#eee; opacity:.45; }
|
||
/* Traegt die Lupe und die Login/Logout-Pille zusammen als eine Einheit
|
||
rechts — nur eine gemeinsame margin-left:auto, statt jedes Element
|
||
einzeln an den Rand zu schieben, sonst klafft zwischen beiden eine
|
||
Luecke statt "direkt links neben der Pille". */
|
||
.rm-crumbs .rm-auth-group { flex:none; margin-left:auto;
|
||
display:flex; align-items:center; gap:6px; }
|
||
.rm-crumbs a.rm-auth { flex:none; padding:6px 12px !important;
|
||
font-size:13px !important; border-radius:99px;
|
||
background:rgba(128,128,128,.18);
|
||
border:1px solid rgba(160,160,160,.4); }
|
||
.rm-crumbs a.rm-search { flex:none; min-width:44px; height:29px;
|
||
display:flex; align-items:center; justify-content:center;
|
||
padding:0 16px !important; font-size:14px; border-radius:99px;
|
||
background:rgba(128,128,128,.18);
|
||
border:1px solid rgba(160,160,160,.4); }
|
||
/* Nur gesetzt, wenn die Leiste wirklich gebaut wurde — ohne Brotkrumen
|
||
im Original-DOM bleibt der Platz sonst leer reserviert. */
|
||
body.rm-crumbed .rm-root {
|
||
padding-top:calc(45px + env(safe-area-inset-top)) !important; }
|
||
body.rm-crumbed .rm-dbg {
|
||
margin-top:calc(45px + env(safe-area-inset-top)); }
|
||
|
||
/* Zweite Leiste unter den Brotkrumen, nur im Thread: Ersatz fuer
|
||
"Search this topic..." der Originalseite. Das dortige Formular wird
|
||
unveraendert hierher verschoben statt neu gebaut, wie das
|
||
Login-Formular in .rm-login form. */
|
||
.rm-search-bar { box-sizing:border-box;
|
||
position:fixed; left:0; right:0; z-index:9997;
|
||
top:calc(44px + env(safe-area-inset-top));
|
||
display:flex; align-items:center;
|
||
padding:8px 12px;
|
||
background:rgba(28,28,30,.96);
|
||
-webkit-backdrop-filter:blur(12px);
|
||
backdrop-filter:blur(12px);
|
||
border-bottom:1px solid rgba(128,128,128,.3);
|
||
transform:translateY(-110%);
|
||
transition:transform .18s ease; }
|
||
.rm-search-bar.rm-open { transform:translateY(0); }
|
||
.rm-search-bar form { flex:1; min-width:0;
|
||
display:flex; align-items:center; gap:8px; }
|
||
.rm-search-bar input[type="text"] {
|
||
flex:1; min-width:0; box-sizing:border-box;
|
||
font-size:16px; padding:9px 12px; border-radius:10px;
|
||
color:#eee; background:rgba(120,120,120,.18);
|
||
border:1px solid rgba(160,160,160,.35); }
|
||
.rm-search-bar input[type="submit"] {
|
||
flex:none; font-size:14px; font-weight:650; padding:9px 16px;
|
||
border-radius:10px; color:#fff; border:0;
|
||
background:rgba(120,170,255,.85); }
|
||
|
||
/* === Themen- und Forenliste === */
|
||
|
||
.rm-sec { padding:18px 14px 6px; font-size:12px; font-weight:700;
|
||
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; }
|
||
|
||
/* Foren und Themen stehen auf einer Forenseite in derselben Tabelle
|
||
untereinander. Im Kartenstrom unterscheiden sie sich sonst nur am Text,
|
||
der Uebergang von den Unterforen zu den Themen ist nicht mehr zu sehen. */
|
||
.rm-group { display:flex; align-items:baseline; gap:10px;
|
||
padding:11px 14px; font-size:12px; font-weight:700;
|
||
text-transform:uppercase; letter-spacing:.07em; opacity:.75;
|
||
background:rgba(128,128,128,.16);
|
||
border-bottom:1px solid rgba(128,128,128,.28);
|
||
cursor:pointer; -webkit-tap-highlight-color:transparent;
|
||
-webkit-user-select:none; user-select:none; }
|
||
.rm-group:active { background:rgba(128,128,128,.26); }
|
||
.rm-group .rm-gname { flex:1; min-width:0; overflow:hidden;
|
||
text-overflow:ellipsis; white-space:nowrap; }
|
||
.rm-group .rm-count { flex:none; font-weight:600; letter-spacing:0;
|
||
opacity:.7; }
|
||
.rm-caret { flex:none; display:inline-block; font-size:10px; opacity:.7;
|
||
transition:transform .15s; }
|
||
.rm-group.rm-closed .rm-caret { transform:rotate(-90deg); }
|
||
|
||
.rm-card { box-sizing:border-box !important;
|
||
display:block !important;
|
||
width:100% !important; max-width:100% !important;
|
||
margin:0 !important;
|
||
padding:14px 14px 15px !important;
|
||
border-bottom:1px solid rgba(128,128,128,.28) !important; }
|
||
.rm-card:active { background:rgba(128,128,128,.14); }
|
||
.rm-card.rm-forum { border-left:3px solid rgba(120,170,255,.7);
|
||
background:rgba(120,170,255,.07); }
|
||
.rm-main { box-sizing:border-box !important;
|
||
display:block !important;
|
||
width:100% !important; max-width:100% !important;
|
||
text-decoration:none !important; color:inherit !important;
|
||
font-size:20px !important; font-weight:600 !important;
|
||
line-height:1.32 !important;
|
||
white-space:normal !important;
|
||
word-break:normal !important;
|
||
overflow-wrap:break-word !important;
|
||
hyphens:none !important; }
|
||
.rm-badge { display:inline; font-size:11px; font-weight:700;
|
||
text-transform:uppercase; letter-spacing:.04em;
|
||
padding:2px 6px; margin-right:6px; border-radius:4px;
|
||
background:rgba(200,60,60,.18); color:#e06a6a;
|
||
white-space:nowrap; vertical-align:2px; }
|
||
.rm-meta { margin-top:8px; font-size:13px; opacity:.7;
|
||
overflow-wrap:break-word; }
|
||
.rm-meta span + span::before { content:" · "; opacity:.55; }
|
||
.rm-foot { margin-top:5px; display:flex; align-items:center;
|
||
justify-content:space-between; gap:10px; }
|
||
.rm-last { font-size:13px; opacity:.55; min-width:0;
|
||
overflow-wrap:break-word; }
|
||
.rm-jump { flex:none; font-size:12px; padding:5px 10px;
|
||
border:1px solid rgba(128,128,128,.4); border-radius:99px;
|
||
text-decoration:none !important; color:inherit !important;
|
||
white-space:nowrap; }
|
||
/* === Thread === */
|
||
|
||
.rm-topic { box-sizing:border-box; padding:16px 14px 12px;
|
||
font-size:21px; font-weight:650; line-height:1.3;
|
||
overflow-wrap:break-word; }
|
||
|
||
.rm-post { box-sizing:border-box !important;
|
||
width:100% !important; max-width:100% !important;
|
||
padding:14px !important;
|
||
border-top:1px solid rgba(128,128,128,.25) !important; }
|
||
|
||
.rm-head { display:flex; align-items:center; gap:9px;
|
||
margin-bottom:10px; min-width:0; }
|
||
/* Die Masse muessen sich gegen die globale img-Regel durchsetzen. */
|
||
.rm-av { flex:none; width:28px !important; height:28px !important;
|
||
border-radius:50%; object-fit:cover;
|
||
background:rgba(128,128,128,.25); }
|
||
.rm-who { min-width:0; flex:1; }
|
||
.rm-user { font-size:14px; font-weight:650; line-height:1.2;
|
||
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||
.rm-sub { font-size:11px; opacity:.55; line-height:1.3;
|
||
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||
|
||
.rm-body { font-size:17px !important; line-height:1.55 !important;
|
||
overflow-wrap:break-word !important;
|
||
word-break:normal !important; }
|
||
.rm-body * { max-width:100% !important; }
|
||
.rm-body img { height:auto !important; border-radius:6px; }
|
||
.rm-body a { overflow-wrap:anywhere !important; }
|
||
.rm-body table { display:block !important; overflow-x:auto !important;
|
||
width:auto !important; }
|
||
.rm-body pre, .rm-body code {
|
||
display:block; overflow-x:auto; white-space:pre;
|
||
font-size:13px; padding:10px; border-radius:8px;
|
||
background:rgba(128,128,128,.14); }
|
||
.rm-body blockquote, .rm-body .quote, .rm-body table.quote {
|
||
display:block !important; margin:12px 0 !important;
|
||
padding:8px 0 8px 12px !important;
|
||
border-left:3px solid rgba(140,140,255,.5) !important;
|
||
font-size:15px !important; opacity:.8 !important;
|
||
background:none !important; }
|
||
.rm-body ul, .rm-body ol { padding-left:22px !important; }
|
||
.rm-body hr { border:0; border-top:1px solid rgba(128,128,128,.3); }
|
||
|
||
.rm-sig { margin-top:12px; font-size:13px; opacity:.5; }
|
||
.rm-sig summary { cursor:pointer; opacity:.8; }
|
||
|
||
/* === Login === */
|
||
|
||
.rm-login-title { padding:16px 14px 6px; font-size:21px;
|
||
font-weight:650; line-height:1.3; }
|
||
.rm-login-note { margin:8px 14px 4px; padding:10px 12px;
|
||
font-size:13px; opacity:.75; border-radius:10px;
|
||
background:rgba(128,128,128,.14);
|
||
overflow-wrap:break-word; }
|
||
.rm-login-note summary { cursor:pointer; font-weight:650; opacity:.9; }
|
||
.rm-login-note[open] summary { margin-bottom:8px; }
|
||
|
||
/* Die verschobene td.row1 wird per Klasse zum Block, wie form/table/tr
|
||
ueber .rm-login form weiter unten. */
|
||
.rm-login-msg { display:block !important; box-sizing:border-box;
|
||
padding:22px 14px calc(60px + env(safe-area-inset-bottom)) !important;
|
||
text-align:center; font-size:16px; line-height:1.6; }
|
||
.rm-login-msg a { display:inline-block; margin-top:8px; padding:12px 24px;
|
||
font-size:16px; font-weight:650; border-radius:10px;
|
||
color:#fff !important; text-decoration:none !important;
|
||
background:rgba(120,170,255,.85); }
|
||
|
||
/* Das Original-Formular ist tabellenbasiert und wird fuer den gestapelten
|
||
Kartenstil komplett auf Block-Elemente umgelegt — derselbe Trick wie bei
|
||
.rm-body table im Thread. Ohne Paginierung fehlt hier die .rm-nav, die
|
||
sonst per .rm-root.rm-paged den unteren Rand freihaelt. Der grosse
|
||
Bodensatz ist kein Stilmittel, sondern erzwingt genug Seitenhoehe zum
|
||
Scrollen: ist die Seite so kurz wie der Viewport, bleibt Safaris eigene,
|
||
nur teilweise eingefahrene Werkzeugleiste unten stehen und verdeckt den
|
||
Button dauerhaft, weil nichts mehr zu scrollen ist, um sie wegzuschieben. */
|
||
.rm-login form { display:block;
|
||
padding:4px 14px calc(140px + env(safe-area-inset-bottom)); }
|
||
.rm-login form table, .rm-login form tbody,
|
||
.rm-login form tr, .rm-login form td {
|
||
display:block !important; width:100% !important; }
|
||
.rm-login form th { display:none !important; }
|
||
.rm-login form td { padding:0 !important; }
|
||
.rm-login form b.gensmall { display:block; margin:16px 0 6px;
|
||
font-size:13px; font-weight:650; opacity:.8; }
|
||
.rm-login form input[type="text"],
|
||
.rm-login form input[type="password"] {
|
||
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-login form span.gensmall, .rm-login form a.gensmall {
|
||
display:inline-block; margin-top:6px; font-size:12px; opacity:.6;
|
||
text-decoration:none !important; color:inherit !important; }
|
||
.rm-login form input[type="checkbox"] { width:18px; height:18px;
|
||
vertical-align:middle; margin-right:8px; }
|
||
.rm-login form input[type="submit"] {
|
||
width:100%; font-size:16px; font-weight:650; padding:13px;
|
||
margin-top:20px; border-radius:10px; color:#fff; border:0;
|
||
background:rgba(120,170,255,.85); }
|
||
|
||
/* === Seitennavigation, beide Ansichten === */
|
||
|
||
.rm-nav { position:fixed; left:0; right:0; bottom:0; z-index:9999;
|
||
display:flex; align-items:center; justify-content:space-between;
|
||
gap:8px; padding:10px 12px calc(10px + env(safe-area-inset-bottom));
|
||
background:rgba(28,28,30,.94);
|
||
-webkit-backdrop-filter:blur(12px);
|
||
backdrop-filter:blur(12px);
|
||
border-top:1px solid rgba(128,128,128,.3); }
|
||
.rm-nav a, .rm-nav span { font-size:14px; color:#eee !important;
|
||
text-decoration:none !important;
|
||
white-space:nowrap; }
|
||
.rm-nav a { padding:7px 13px; border-radius:99px;
|
||
border:1px solid rgba(160,160,160,.45); }
|
||
.rm-nav .rm-step { min-width:22px; text-align:center;
|
||
font-size:17px; line-height:1; padding:9px 12px; }
|
||
.rm-nav .rm-pos { opacity:.6; font-size:13px; }
|
||
.rm-nav .rm-pos-btn { opacity:1; padding:7px 12px; border-radius:99px;
|
||
border:1px solid rgba(160,160,160,.45);
|
||
cursor:pointer; }
|
||
.rm-nav .rm-off { opacity:.28; border-color:rgba(160,160,160,.2); }
|
||
|
||
.rm-sheet-bg { position:fixed; top:0; left:0; right:0; bottom:0;
|
||
z-index:10000; background:rgba(0,0,0,.5);
|
||
opacity:0; pointer-events:none; transition:opacity .18s; }
|
||
.rm-sheet-bg.rm-open { opacity:1; pointer-events:auto; }
|
||
|
||
.rm-sheet { box-sizing:border-box;
|
||
position:fixed; left:0; right:0; bottom:0; z-index:10001;
|
||
display:flex; flex-direction:column; max-height:72vh;
|
||
padding:8px 12px calc(12px + env(safe-area-inset-bottom));
|
||
color:#eee; background:rgba(28,28,30,.98);
|
||
-webkit-backdrop-filter:blur(14px);
|
||
backdrop-filter:blur(14px);
|
||
border-top:1px solid rgba(128,128,128,.3);
|
||
border-radius:14px 14px 0 0;
|
||
transform:translateY(102%);
|
||
transition:transform .22s ease; }
|
||
.rm-sheet.rm-open { transform:translateY(0); }
|
||
.rm-grip { flex:none; width:36px; height:4px; margin:2px auto 12px;
|
||
border-radius:2px; background:rgba(200,200,200,.35); }
|
||
.rm-goto { flex:none; display:flex; gap:8px; margin-bottom:12px; }
|
||
/* 16px verhindert, dass iOS beim Fokussieren in das Feld hineinzoomt. */
|
||
.rm-goto input { 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-goto button { flex:none; font-size:14px; padding:9px 18px;
|
||
border-radius:10px; color:#eee;
|
||
background:rgba(120,120,120,.18);
|
||
border:1px solid rgba(160,160,160,.45); }
|
||
.rm-grid { flex:1; min-height:0; overflow-y:auto;
|
||
-webkit-overflow-scrolling:touch;
|
||
display:grid; gap:8px; padding-bottom:4px;
|
||
grid-template-columns:repeat(auto-fill, minmax(56px, 1fr)); }
|
||
.rm-grid a { display:flex; align-items:center; justify-content:center;
|
||
min-height:42px; font-size:14px; border-radius:10px;
|
||
color:#eee !important; text-decoration:none !important;
|
||
background:rgba(120,120,120,.12);
|
||
border:1px solid rgba(160,160,160,.3); }
|
||
.rm-grid a.rm-cur { font-weight:700;
|
||
background:rgba(120,170,255,.22);
|
||
border-color:rgba(120,170,255,.9); }
|
||
`;
|
||
h().appendChild(st);
|
||
}
|
||
|
||
// Originalinhalt ausblenden und die neue Ansicht als einziges sichtbares
|
||
// body-Kind einhaengen. Alles, was spaeter noch an body soll, muss nach
|
||
// diesem Aufruf kommen.
|
||
function replaceBody(root) {
|
||
Array.prototype.forEach.call(document.body.children, function (el) {
|
||
if (el !== root) el.classList.add('rm-hidden');
|
||
});
|
||
document.body.appendChild(root);
|
||
}
|
||
|
||
// Ob eine Sitzung besteht, verraet nur der Menuebalken der Originalseite:
|
||
// ausgeloggt verlinkt er ucp.php?mode=login, eingeloggt stattdessen
|
||
// mode=logout, ueblicherweise als "Logout [ Name ]". Das Namensmuster ist
|
||
// ungetestet, da dem Projekt keine eingeloggte Fixture vorliegt — schlaegt
|
||
// es fehl, bleibt die Pille ohne Namen stehen statt zu reissen.
|
||
function loginState() {
|
||
var out = document.querySelector('a[href*="mode=logout"]');
|
||
if (out) {
|
||
var m = txt(out).match(/\[\s*(.+?)\s*\]/);
|
||
return { in: true, href: out.href, name: m ? m[1] : '' };
|
||
}
|
||
var into = document.querySelector('a[href*="mode=login"]');
|
||
return { in: false, href: into ? into.href : './ucp.php?mode=login' };
|
||
}
|
||
|
||
// Ohne die Brotkrumen fuehrt aus der neuen Ansicht kein Weg zurueck: das
|
||
// Original-DOM ist ausgeblendet, und phpBB verlinkt das Elternforum nirgends
|
||
// sonst. Muss nach replaceBody() laufen, weil die Leiste an body haengt.
|
||
function addCrumbs() {
|
||
// Die Leiste steht zweimal im Markup, ueber und unter dem Inhalt. Die
|
||
// erste genuegt; a.breadcrumbs faengt Seiten ohne den Absatz drumherum.
|
||
var box = document.querySelector('p.breadcrumbs');
|
||
var items = box ? box.querySelectorAll('a')
|
||
: document.querySelectorAll('a.breadcrumbs');
|
||
|
||
var bar = document.createElement('div');
|
||
bar.className = 'rm-crumbs';
|
||
var n = 0;
|
||
Array.prototype.forEach.call(items, function (a) {
|
||
var t = txt(a);
|
||
if (!t) return;
|
||
if (n) {
|
||
var sep = document.createElement('span');
|
||
sep.className = 'rm-sep';
|
||
sep.textContent = '›';
|
||
bar.appendChild(sep);
|
||
}
|
||
var l = document.createElement('a');
|
||
l.href = a.href;
|
||
l.textContent = t;
|
||
bar.appendChild(l);
|
||
n++;
|
||
});
|
||
|
||
log.push('krumen=' + n);
|
||
if (!n) return;
|
||
|
||
var st = loginState();
|
||
var group = document.createElement('div');
|
||
group.className = 'rm-auth-group';
|
||
|
||
// Lupe nur bei bestehender Sitzung. Ausserhalb von viewtopic fuehrt sie
|
||
// direkt auf search.php, im Thread blendet sie stattdessen die zweite
|
||
// Leiste ein — Ersatz fuer "Search this topic...", das mit dem
|
||
// Original-DOM ausgeblendet wurde.
|
||
if (st.in) {
|
||
var search = document.createElement('a');
|
||
search.className = 'rm-search';
|
||
search.href = 'https://cs.rin.ru/forum/search.php';
|
||
search.textContent = '🔍';
|
||
search.setAttribute('aria-label', 'Suche');
|
||
search.addEventListener('click', function (e) {
|
||
if (view === 'thread') { e.preventDefault(); toggleThreadSearch(); }
|
||
});
|
||
group.appendChild(search);
|
||
}
|
||
|
||
var auth = document.createElement('a');
|
||
auth.className = 'rm-auth';
|
||
auth.href = st.href;
|
||
auth.textContent = st.in ? (st.name || 'Logout') : 'Login';
|
||
group.appendChild(auth);
|
||
bar.appendChild(group);
|
||
|
||
document.body.appendChild(bar);
|
||
document.body.classList.add('rm-crumbed');
|
||
}
|
||
|
||
// ==========================================================================
|
||
// Seitennavigation, beide Ansichten
|
||
// ==========================================================================
|
||
|
||
// Foren- und Themenseiten paginieren gleich: derselbe start-Parameter,
|
||
// dieselbe jumpto()-Funktion mit demselben per_page darin. Der Block gilt
|
||
// deshalb fuer beide Ansichten.
|
||
|
||
// --- Seitenzustand ---
|
||
// Der Block wird aus dem Original-DOM gelesen, bevor es ausgeblendet wird.
|
||
// phpBB zeigt nie alle Seitenzahlen an, deshalb wird nicht auf gerenderte
|
||
// Links gesetzt, sondern jede Ziel-URL aus start = (Seite - 1) * per_page
|
||
// selbst gebaut.
|
||
function pageState() {
|
||
var s = { cur: 1, total: 1, per: 0, prev: null, next: null };
|
||
|
||
// Der Paginierungsblock ist der einzige Ort, an dem Ziffern-Links
|
||
// Seitenzahlen bedeuten. Beitragstexte enthalten ebenfalls Links mit
|
||
// start=, die hier nicht mitgezaehlt werden duerfen.
|
||
var box = null;
|
||
var jumpA = document.querySelector('a[onclick*="jumpto"]');
|
||
if (jumpA) box = jumpA.parentElement;
|
||
if (!box) {
|
||
document.querySelectorAll('a[href*="start="]').forEach(function (a) {
|
||
if (!box && /^(Next|Weiter|Nächste)$/i.test(txt(a)))
|
||
box = a.parentElement;
|
||
});
|
||
}
|
||
|
||
// per_page steht als Literal in der jumpto()-Funktion der Seite.
|
||
document.querySelectorAll('script').forEach(function (el) {
|
||
if (s.per) return;
|
||
var m2 = (el.textContent || '').match(/per_page\s*=\s*'?(\d+)/);
|
||
if (m2) s.per = parseInt(m2[1], 10);
|
||
});
|
||
|
||
if (box) {
|
||
box.querySelectorAll('a[href*="start="]').forEach(function (a) {
|
||
var t = txt(a);
|
||
if (!/^\d+$/.test(t)) return;
|
||
var n = parseInt(t, 10);
|
||
if (n > s.total) s.total = n;
|
||
var ms = a.href.match(/[?&]start=(\d+)/);
|
||
if (!s.per && ms && n > 1) {
|
||
var per = parseInt(ms[1], 10) / (n - 1);
|
||
if (per > 0 && per === Math.floor(per)) s.per = per;
|
||
}
|
||
});
|
||
}
|
||
|
||
var cs = location.search.match(/[?&]start=(\d+)/);
|
||
if (s.per) s.cur = Math.floor((cs ? parseInt(cs[1], 10) : 0) / s.per) + 1;
|
||
else if (box) {
|
||
var strong = box.querySelector('strong, b > strong');
|
||
if (strong && /^\d+$/.test(txt(strong))) s.cur = parseInt(txt(strong), 10);
|
||
}
|
||
|
||
// Aeltere Themes geben stattdessen "Page X of Y" aus.
|
||
var pos = txt(document.body).match(/Page\s+(\d+)\s+of\s+(\d+)/i);
|
||
if (pos) {
|
||
if (!s.per) s.cur = parseInt(pos[1], 10);
|
||
s.total = Math.max(s.total, parseInt(pos[2], 10));
|
||
}
|
||
if (s.cur > s.total) s.total = s.cur;
|
||
|
||
s.url = function (n) {
|
||
if (n === s.cur || n < 1 || n > s.total) return null;
|
||
if (n !== 1 && !s.per) return null;
|
||
var u = location.href.split('#')[0];
|
||
var start = (n - 1) * s.per;
|
||
if (/[?&]start=\d+/.test(u))
|
||
return u.replace(/([?&]start=)\d+/, '$1' + start);
|
||
return u + (u.indexOf('?') === -1 ? '?' : '&') + 'start=' + start;
|
||
};
|
||
|
||
s.prev = s.url(s.cur - 1);
|
||
s.next = s.url(s.cur + 1);
|
||
// Ohne bekanntes per_page bleibt nur, die Original-Links zu uebernehmen.
|
||
if (!s.per) {
|
||
document.querySelectorAll('a').forEach(function (a) {
|
||
var t = txt(a);
|
||
if (!s.prev && /^(Previous|Vorherige)$/i.test(t)) s.prev = a.href;
|
||
if (!s.next && /^(Next|Weiter|Nächste)$/i.test(t)) s.next = a.href;
|
||
});
|
||
}
|
||
|
||
log.push('page=' + s.cur + '/' + s.total + ' per=' + s.per);
|
||
return s;
|
||
}
|
||
|
||
// Seitenwahl als Sheet ueber der Leiste. Wird erst beim ersten Antippen
|
||
// gebaut: bei 1000+ Seiten waere das Raster sonst umsonst im DOM.
|
||
function pageSheet(s) {
|
||
var bg = null, sheet = null;
|
||
|
||
function go(n) {
|
||
n = Math.min(Math.max(n, 1), s.total);
|
||
var u = s.url(n);
|
||
if (u) location.href = u;
|
||
else close();
|
||
}
|
||
|
||
function close() {
|
||
if (!bg) return;
|
||
bg.classList.remove('rm-open');
|
||
sheet.classList.remove('rm-open');
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
function make() {
|
||
bg = document.createElement('div');
|
||
bg.className = 'rm-sheet-bg';
|
||
bg.addEventListener('click', close);
|
||
|
||
sheet = document.createElement('div');
|
||
sheet.className = 'rm-sheet';
|
||
|
||
var grip = document.createElement('div');
|
||
grip.className = 'rm-grip';
|
||
sheet.appendChild(grip);
|
||
|
||
var row = document.createElement('div');
|
||
row.className = 'rm-goto';
|
||
var inp = document.createElement('input');
|
||
inp.type = 'text';
|
||
inp.inputMode = 'numeric';
|
||
inp.pattern = '[0-9]*';
|
||
inp.placeholder = 'Seite 1 – ' + s.total;
|
||
inp.setAttribute('aria-label', 'Seitenzahl');
|
||
var btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.textContent = 'Los';
|
||
function submit() {
|
||
var n = parseInt(inp.value, 10);
|
||
if (n > 0) go(n);
|
||
}
|
||
btn.addEventListener('click', submit);
|
||
inp.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
||
});
|
||
row.appendChild(inp);
|
||
row.appendChild(btn);
|
||
sheet.appendChild(row);
|
||
|
||
var grid = document.createElement('div');
|
||
grid.className = 'rm-grid';
|
||
var curEl = null;
|
||
for (var i = 1; i <= s.total; i++) {
|
||
var a = document.createElement('a');
|
||
a.textContent = i;
|
||
if (i === s.cur) { a.className = 'rm-cur'; curEl = a; }
|
||
else {
|
||
var u = s.url(i);
|
||
if (u) a.href = u;
|
||
}
|
||
grid.appendChild(a);
|
||
}
|
||
sheet.appendChild(grid);
|
||
|
||
document.body.appendChild(bg);
|
||
document.body.appendChild(sheet);
|
||
return curEl;
|
||
}
|
||
|
||
return function open() {
|
||
var curEl = bg ? null : make();
|
||
document.body.style.overflow = 'hidden';
|
||
// Ein Frame Abstand, sonst faellt der Uebergang beim ersten Oeffnen aus.
|
||
requestAnimationFrame(function () {
|
||
bg.classList.add('rm-open');
|
||
sheet.classList.add('rm-open');
|
||
if (curEl) curEl.scrollIntoView({ block: 'center' });
|
||
});
|
||
};
|
||
}
|
||
|
||
// Die fixierte Leiste am unteren Rand. Der Aufrufer reicht den Zustand
|
||
// herein, weil er allein entscheidet, ob die Leiste ueberhaupt lohnt: im
|
||
// Thread gehoert sie zum Geruest, eine einseitige Liste braucht sie nicht.
|
||
function pageNav(root, pg) {
|
||
var nav = document.createElement('div');
|
||
nav.className = 'rm-nav';
|
||
|
||
function navBtn(href, label, aria) {
|
||
var e = document.createElement('a');
|
||
e.className = 'rm-step' + (href ? '' : ' rm-off');
|
||
e.textContent = label;
|
||
e.setAttribute('aria-label', aria);
|
||
if (href) e.href = href;
|
||
return e;
|
||
}
|
||
nav.appendChild(navBtn(pg.url(1), '«', 'Erste Seite'));
|
||
nav.appendChild(navBtn(pg.prev, '‹', 'Vorherige Seite'));
|
||
|
||
var pos = document.createElement('span');
|
||
pos.className = 'rm-pos';
|
||
pos.textContent = pg.cur + ' / ' + pg.total;
|
||
// Ohne bekanntes per_page laesst sich kein Sprungziel bauen, dann bleibt
|
||
// die Anzeige reine Beschriftung.
|
||
if (pg.total > 1 && pg.per) {
|
||
pos.className += ' rm-pos-btn';
|
||
pos.textContent += ' ▾';
|
||
pos.setAttribute('role', 'button');
|
||
pos.setAttribute('aria-label', 'Seite wählen');
|
||
pos.addEventListener('click', pageSheet(pg));
|
||
}
|
||
nav.appendChild(pos);
|
||
|
||
nav.appendChild(navBtn(pg.next, '›', 'Nächste Seite'));
|
||
nav.appendChild(navBtn(pg.url(pg.total), '»', 'Letzte Seite'));
|
||
|
||
// Die Freihaltung haengt an der Leiste selbst: ohne sie stuende unten ein
|
||
// leerer Streifen.
|
||
root.classList.add('rm-paged');
|
||
root.appendChild(nav);
|
||
}
|
||
|
||
// ==========================================================================
|
||
// Themenliste und Forenliste
|
||
// ==========================================================================
|
||
|
||
function breakable(s) {
|
||
return s.replace(/([|\/\\,;:>\]\)])/g, '$1\u200B');
|
||
}
|
||
|
||
// Karten teilen sich Titel, Zahlenzeile und Fusszeile; nur die Quellen
|
||
// dafuer unterscheiden sich zwischen Thema und Forum.
|
||
function card(titleA, badgeable, kind) {
|
||
var el = document.createElement('div');
|
||
el.className = 'rm-card' + (kind === 'forum' ? ' rm-forum' : '');
|
||
|
||
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;
|
||
|
||
var titleCell = titleA.closest('td');
|
||
var nums = [], author = '', last = '', jump = null;
|
||
|
||
Array.prototype.forEach.call(tr.children, function (td) {
|
||
if (td === titleCell) return;
|
||
var t = txt(td);
|
||
if (/^\d+$/.test(t)) { nums.push(t); return; }
|
||
var pa = td.querySelector('a[href*="profile.php"],a[href*="memberlist.php"]');
|
||
if (pa && t.length < 40 && !/\d{4}/.test(t)) { author = t; return; }
|
||
if (t) {
|
||
last = t;
|
||
jump = td.querySelector('a[href*="#p"]') ||
|
||
td.querySelector('a[href*="viewtopic.php"]');
|
||
}
|
||
});
|
||
|
||
var el = card(titleA, true, 'thema');
|
||
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;
|
||
}
|
||
|
||
// 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 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, 'forum');
|
||
|
||
// 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;
|
||
});
|
||
}
|
||
// 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);
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Ankuendigungen und Stickies stehen vor jedem Thema jeder Seite eines
|
||
// Forums. Ihr Typ steht im Symbol der ersten Spalte, nicht am Titel:
|
||
// global_read.png, announce_read.png, sticky_read.png gegen topic_read.png.
|
||
function topicGroup(tr) {
|
||
var im = tr.querySelector('img[src]');
|
||
var m = im && (im.getAttribute('src') || '')
|
||
.match(/\b(global|announce|sticky)_(?:un)?read/);
|
||
return m ? m[1] : 'thema';
|
||
}
|
||
|
||
// phpBB stellt jeder dieser Gruppen eine Trennzeile voran. Sie traegt keinen
|
||
// Link und faellt deshalb beim Einsammeln durch — ihr Text ist aber die
|
||
// ehrlichste Beschriftung, die es fuer die Gruppe gibt, und deckt auch
|
||
// Abschnitte ab, die dieses Script nicht kennt.
|
||
function sepLabel(tr) {
|
||
var p = tr.previousElementSibling, n = 0;
|
||
while (p && n++ < 4) {
|
||
if (p.dataset.rm) return '';
|
||
if (p.children.length === 1 && p.children[0].hasAttribute('colspan') &&
|
||
!p.querySelector('a, table')) {
|
||
var t = txt(p);
|
||
if (t && t.length < 40) return t;
|
||
}
|
||
p = p.previousElementSibling;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
var GNAME = { forum: 'Foren', thema: 'Themen', sticky: 'Stickies',
|
||
global: 'Globale Ankündigungen', announce: 'Ankündigungen' };
|
||
// Genau die Gruppen, die zwischen dem Leser und den eigentlichen Themen
|
||
// stehen, starten zugeklappt.
|
||
var GSHUT = { global: 1, announce: 1, sticky: 1 };
|
||
|
||
// Der Zustand liegt in localStorage und nicht in GM_setValue: das kommt ohne
|
||
// @grant aus und verhaelt sich in Tampermonkey und der iOS-App gleich. Der
|
||
// Schluessel ist die Gruppenart, nicht ihr Name — einmal zugeklappt gilt
|
||
// fuer jedes Forum.
|
||
var GKEY = 'rm-groups';
|
||
|
||
function groupState() {
|
||
try { return JSON.parse(localStorage.getItem(GKEY)) || {}; }
|
||
catch (e) { return {}; }
|
||
}
|
||
|
||
function saveGroupState(group, open) {
|
||
try {
|
||
var s = groupState();
|
||
s[group] = open;
|
||
localStorage.setItem(GKEY, JSON.stringify(s));
|
||
} catch (e) { log.push('speichern: ' + e.message); }
|
||
}
|
||
|
||
// Kopf ueber einem Block gleichartiger Karten, dazu der Kasten, in den sie
|
||
// gehoeren: die Originalseite trennt Foren, Ankuendigungen, Stickies und
|
||
// Themen ueber eigene Kopfzeilen, im Kartenstrom geht diese Grenze verloren.
|
||
// Der Kopf ist zugleich der Schalter, der den Block zuklappt.
|
||
function openGroup(root, group, label, n) {
|
||
var saved = groupState()[group];
|
||
var open = saved === undefined ? !GSHUT[group] : !!saved;
|
||
|
||
var head = document.createElement('div');
|
||
head.className = 'rm-group';
|
||
head.setAttribute('role', 'button');
|
||
|
||
var t = document.createElement('span');
|
||
t.className = 'rm-gname';
|
||
t.textContent = label;
|
||
head.appendChild(t);
|
||
|
||
var c = document.createElement('span');
|
||
c.className = 'rm-count';
|
||
c.textContent = n;
|
||
head.appendChild(c);
|
||
|
||
var caret = document.createElement('span');
|
||
caret.className = 'rm-caret';
|
||
caret.textContent = '▼';
|
||
head.appendChild(caret);
|
||
|
||
var body = document.createElement('div');
|
||
body.className = 'rm-gbody';
|
||
|
||
function apply() {
|
||
head.classList.toggle('rm-closed', !open);
|
||
body.classList.toggle('rm-hidden', !open);
|
||
head.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||
}
|
||
apply();
|
||
|
||
head.addEventListener('click', function () {
|
||
open = !open;
|
||
apply();
|
||
saveGroupState(group, open);
|
||
});
|
||
|
||
root.appendChild(head);
|
||
root.appendChild(body);
|
||
return body;
|
||
}
|
||
|
||
// Welche Karte eine Zeile ergibt, entscheidet der erste verwertbare Link
|
||
// darin. Beiwerk wie die Brotkrumen 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 '';
|
||
// "Mark topics read" (eingeloggt oberhalb der Themenliste) und "Mark
|
||
// forums read" verlinken mit ?mark=topics/forums auf die aktuelle Seite
|
||
// selbst. Der Link liegt zwar in einer eigenen inneren <tr> ohne
|
||
// verschachtelte Tabelle, .closest('td.cat') findet aber trotzdem die
|
||
// aeussere Kategoriezelle und machte daraus bislang eine dauerhafte,
|
||
// nicht einklappbare .rm-sec-Karte statt gar keine.
|
||
if (/[?&]mark=/.test(a.getAttribute('href') || '')) return '';
|
||
if (/viewtopic\.php/.test(a.getAttribute('href') || '')) {
|
||
if (tr.querySelector('a.forumlink')) return 'forum';
|
||
// Der Menuebalken der Seite verlinkt "Forum rules" und die FAQ ebenfalls
|
||
// auf Themen. Die Bremse fuer Layoutzeilen weiter unten greift dort
|
||
// nicht, weil die Zeile weder Tabelle noch Script enthaelt — es fehlen
|
||
// ihr aber die Merkmale einer Themenzeile.
|
||
return tr.querySelector('a.topictitle') || numCells(tr) >= 2 ? '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.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;
|
||
var group = kind === 'sektion' ? ''
|
||
: kind === 'forum' ? 'forum'
|
||
: topicGroup(tr);
|
||
// Vor dem Setzen der Marke: sepLabel liest sie an den Zeilen darueber,
|
||
// um an einer bereits verwerteten Zeile stehen zu bleiben.
|
||
var label = sepLabel(tr) || GNAME[group] || GNAME.thema;
|
||
tr.dataset.rm = '1';
|
||
rows.push({ tr: tr, a: a, kind: kind, group: group, label: label });
|
||
});
|
||
log.push('zeilen=' + rows.length);
|
||
if (!rows.length) return;
|
||
|
||
var root = document.createElement('div');
|
||
root.className = 'rm-root';
|
||
|
||
// Erst alle Karten bauen, dann ausgeben: der Gruppenkopf nennt die Zahl der
|
||
// Karten unter sich, und die steht erst fest, wenn die Zeilen durch sind,
|
||
// von denen manche keine Karte hergeben.
|
||
var items = [];
|
||
rows.forEach(function (r) {
|
||
try {
|
||
var c = r.kind === 'thema' ? convertTopic(r.tr)
|
||
: r.kind === 'forum' ? convertForum(r.tr)
|
||
: section(r.a);
|
||
if (c) items.push({ kind: r.kind, group: r.group, label: r.label, el: c });
|
||
} catch (e) { log.push('convert: ' + e.message); }
|
||
});
|
||
|
||
var built = 0, last = '', box = null;
|
||
items.forEach(function (it, i) {
|
||
if (it.kind === 'sektion') {
|
||
root.appendChild(it.el);
|
||
last = 'sektion';
|
||
return;
|
||
}
|
||
built++;
|
||
if (it.group !== last) {
|
||
// Eine Kategoriezeile leitet die Foren darunter bereits ein — auf der
|
||
// Forenwurzel wuerde ein Gruppenkopf sie nur doppeln.
|
||
if (last === 'sektion') box = root;
|
||
else {
|
||
var n = 0;
|
||
while (i + n < items.length && items[i + n].group === it.group) n++;
|
||
box = openGroup(root, it.group, it.label, n);
|
||
}
|
||
last = it.group;
|
||
}
|
||
box.appendChild(it.el);
|
||
});
|
||
log.push('karten=' + built);
|
||
if (!built) return;
|
||
|
||
// Muss vor replaceBody() laufen: pageState() liest das Original-DOM, und
|
||
// "Page X of Y" wuerde sonst im Text der eigenen Karten gesucht.
|
||
var pg = pageState();
|
||
if (pg.total > 1) pageNav(root, pg);
|
||
|
||
replaceBody(root);
|
||
addCrumbs();
|
||
|
||
if (DEBUG) {
|
||
var c0 = root.querySelector('.rm-card');
|
||
log.push('fenster=' + window.innerWidth +
|
||
' body=' + document.body.scrollWidth +
|
||
' karte=' + (c0 ? Math.round(c0.getBoundingClientRect().width) : '?'));
|
||
}
|
||
}
|
||
|
||
// ==========================================================================
|
||
// 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';
|
||
|
||
var bar = document.createElement('div');
|
||
bar.className = 'rm-search-bar';
|
||
bar.appendChild(form);
|
||
document.body.appendChild(bar);
|
||
return bar;
|
||
}
|
||
|
||
function toggleThreadSearch() {
|
||
if (!threadSearchBar) threadSearchBar = buildThreadSearch();
|
||
if (!threadSearchBar) return;
|
||
var open = threadSearchBar.classList.toggle('rm-open');
|
||
if (open) {
|
||
var inp = threadSearchBar.querySelector('input[type="text"]');
|
||
if (inp) inp.focus();
|
||
}
|
||
}
|
||
|
||
// Der Beitragstext liegt in einer verschachtelten Tabelle. closest('tr')
|
||
// trifft deshalb die innere Zeile, der Autor- und Profilspalte fehlen.
|
||
// Aufsteigen, bis die Zeile eine Profil- oder Autorenspalte enthaelt.
|
||
function postRow(pb) {
|
||
var tr = pb.closest('tr');
|
||
while (tr) {
|
||
if (tr.querySelector(':scope > td.profile') ||
|
||
tr.querySelector(':scope > td .postauthor')) return tr;
|
||
var up = tr.parentElement ? tr.parentElement.closest('tr') : null;
|
||
if (!up) return tr;
|
||
tr = up;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function buildThread() {
|
||
var bodies = document.querySelectorAll('span.postbody, .postbody');
|
||
log.push('postbodies=' + bodies.length);
|
||
if (!bodies.length) return;
|
||
|
||
var root = document.createElement('div');
|
||
root.className = 'rm-root';
|
||
|
||
var tt = document.querySelector('a.maintitle, h1, .maintitle');
|
||
if (tt) {
|
||
var th = document.createElement('div');
|
||
th.className = 'rm-topic';
|
||
th.textContent = txt(tt);
|
||
root.appendChild(th);
|
||
}
|
||
|
||
var built = 0, seen = new Set();
|
||
|
||
bodies.forEach(function (pb) {
|
||
var row = postRow(pb);
|
||
if (!row || seen.has(row)) return;
|
||
seen.add(row);
|
||
|
||
var card = document.createElement('div');
|
||
card.className = 'rm-post';
|
||
|
||
// --- Zellen dieser Zeile + der Kopfzeilen darueber ---
|
||
var cells = [];
|
||
Array.prototype.forEach.call(row.children, function (td) {
|
||
if (!td.contains(pb)) cells.push(td);
|
||
});
|
||
var pr = row.previousElementSibling, g = 0;
|
||
while (pr && g++ < 3) {
|
||
Array.prototype.forEach.call(pr.children, function (td) {
|
||
cells.push(td);
|
||
});
|
||
if (pr.querySelector('.postauthor, a[href*="profile.php"]')) break;
|
||
pr = pr.previousElementSibling;
|
||
}
|
||
|
||
// --- Username ---
|
||
var user = '';
|
||
for (var i = 0; i < cells.length && !user; i++) {
|
||
var ua = cells[i].querySelector(
|
||
'.postauthor, .username-coloured, a[href*="profile.php"]');
|
||
if (ua) user = txt(ua);
|
||
}
|
||
if (!user) {
|
||
for (var i2 = 0; i2 < cells.length && !user; i2++) {
|
||
var nm = cells[i2].querySelector('span.name b, span.name, b.name');
|
||
if (nm) user = txt(nm);
|
||
}
|
||
}
|
||
user = user.replace(/^Post subject:.*/i, '').trim();
|
||
|
||
// --- Avatar ---
|
||
var avImg = null, bestArea = 0;
|
||
cells.forEach(function (td) {
|
||
td.querySelectorAll('img').forEach(function (im) {
|
||
var s = im.getAttribute('src') || '';
|
||
if (/spacer|blank|icon|rank|star|folder|button|pm|email|quote|edit|report|www|msn|aim|yim|icq|topic/i.test(s))
|
||
return;
|
||
if (/avatar/i.test(s)) { avImg = im; bestArea = 1e9; return; }
|
||
var w = parseInt(im.getAttribute('width'), 10) || im.naturalWidth || 0;
|
||
var hh = parseInt(im.getAttribute('height'), 10) || im.naturalHeight || 0;
|
||
var a = w * hh;
|
||
if (w >= 40 && a > bestArea) { avImg = im; bestArea = a; }
|
||
});
|
||
});
|
||
|
||
// --- Rang, Postzahl, Zeitstempel ---
|
||
var allTxt = cells.map(txt).join(' ');
|
||
var rank = '';
|
||
var rr = allTxt.match(/\b(Administrator|Site Admin|Global Moderator|Moderator|VIP|Uploader|Supporter|Member|Newbie|Guest)\b/i);
|
||
if (rr) rank = rr[1];
|
||
var pc = allTxt.match(/Posts:\s*([\d.,]+)/i);
|
||
if (pc) rank = (rank ? rank + ' · ' : '') + pc[1] + ' Posts';
|
||
|
||
var when = '';
|
||
var wm = allTxt.match(/Posted:\s*([^|]+?)(?:\s{2,}|Post subject|$)/i);
|
||
if (wm) when = wm[1].trim();
|
||
|
||
// --- Kopfzeile ---
|
||
var head = document.createElement('div');
|
||
head.className = 'rm-head';
|
||
|
||
var av;
|
||
if (avImg) {
|
||
av = document.createElement('img');
|
||
av.src = avImg.src;
|
||
} else {
|
||
av = document.createElement('div');
|
||
av.textContent = (user || '?').charAt(0).toUpperCase();
|
||
av.style.cssText = 'display:flex;align-items:center;' +
|
||
'justify-content:center;font-size:13px;font-weight:700;' +
|
||
'color:rgba(255,255,255,.75);';
|
||
}
|
||
av.className = 'rm-av';
|
||
head.appendChild(av);
|
||
|
||
var who = document.createElement('div');
|
||
who.className = 'rm-who';
|
||
var un = document.createElement('div');
|
||
un.className = 'rm-user';
|
||
un.textContent = user || 'Gast';
|
||
who.appendChild(un);
|
||
var sub = document.createElement('div');
|
||
sub.className = 'rm-sub';
|
||
sub.textContent = [rank, when].filter(Boolean).join(' · ');
|
||
who.appendChild(sub);
|
||
head.appendChild(who);
|
||
card.appendChild(head);
|
||
|
||
// --- Signatur abtrennen ---
|
||
var sig = null, hr = pb.querySelector('hr');
|
||
if (hr) {
|
||
sig = document.createElement('details');
|
||
sig.className = 'rm-sig';
|
||
var sm = document.createElement('summary');
|
||
sm.textContent = 'Signatur';
|
||
sig.appendChild(sm);
|
||
var n = hr.nextSibling;
|
||
while (n) { var nx = n.nextSibling; sig.appendChild(n); n = nx; }
|
||
hr.remove();
|
||
}
|
||
|
||
var bodyWrap = document.createElement('div');
|
||
bodyWrap.className = 'rm-body';
|
||
bodyWrap.appendChild(pb);
|
||
card.appendChild(bodyWrap);
|
||
if (sig && sig.childNodes.length > 1) card.appendChild(sig);
|
||
|
||
root.appendChild(card);
|
||
built++;
|
||
});
|
||
|
||
log.push('posts=' + built);
|
||
if (!built) return;
|
||
|
||
pageNav(root, pageState());
|
||
|
||
replaceBody(root);
|
||
addCrumbs();
|
||
}
|
||
|
||
// ==========================================================================
|
||
// Login
|
||
// ==========================================================================
|
||
|
||
// Das Original-Formular wird unveraendert wiederverwendet statt neu gebaut
|
||
// — wie der Beitragstext im Thread. So bleiben versteckte Felder (sid,
|
||
// redirect) und die serverseitige Validierung erhalten; das Script muss
|
||
// weder Sitzungs- noch Fehlerzustaende selbst nachbilden.
|
||
function buildLogin() {
|
||
var userInput = document.querySelector('input[name="username"]');
|
||
var form = userInput && userInput.closest('form');
|
||
if (!form) { buildLoginInfo(); return; }
|
||
|
||
var root = document.createElement('div');
|
||
root.className = 'rm-root rm-login';
|
||
|
||
var th = form.querySelector('th');
|
||
var title = document.createElement('div');
|
||
title.className = 'rm-login-title';
|
||
title.textContent = txt(th) || 'Login';
|
||
root.appendChild(title);
|
||
|
||
// Der Regeltext steckt als einziger Inhalt in td.row1 und wird separat als
|
||
// aufklappbarer Kasten gefuehrt statt mitten im Formular zu stehen.
|
||
var note = form.querySelector('.postbody');
|
||
if (note && txt(note)) {
|
||
var det = document.createElement('details');
|
||
det.className = 'rm-login-note';
|
||
var sm = document.createElement('summary');
|
||
sm.textContent = 'Regeln';
|
||
det.appendChild(sm);
|
||
det.appendChild(note);
|
||
root.appendChild(det);
|
||
}
|
||
|
||
// Zellen ohne Element-Kinder und ohne Text sind reine Layout-Fuellzellen
|
||
// (blosses " " als Platzhalter neben Checkboxen) — im gestapelten
|
||
// Formular bleiben sie sonst als leere Zeilen stehen.
|
||
Array.prototype.forEach.call(form.querySelectorAll('td'), function (td) {
|
||
if (!td.children.length && !txt(td)) td.classList.add('rm-hidden');
|
||
});
|
||
|
||
root.appendChild(form);
|
||
replaceBody(root);
|
||
addCrumbs();
|
||
log.push('login=formular');
|
||
}
|
||
|
||
// ucp.php?mode=login liefert ausser dem Formular auch Zwischenseiten ueber
|
||
// dieselbe generische "Information"-Vorlage aus — zum Beispiel die
|
||
// Bestaetigung nach erfolgreichem Login, vermutlich aber auch andere
|
||
// Meldungen wie ein falsches Passwort, fuer die keine Fixture vorliegt.
|
||
// Erkannt wird die Vorlage an der Form, nicht am Text: eine Kopfzeile mit
|
||
// genau einer th-Zelle traegt in diesem View sonst nirgends eine einzelne
|
||
// Nachricht samt Link. Wie beim Formular wandert die Original-Zelle
|
||
// unveraendert in die neue Ansicht, damit der Link darin (zurueck zum Index
|
||
// oder zu einem anderen Ziel) unangetastet bleibt.
|
||
function buildLoginInfo() {
|
||
var th = null;
|
||
document.querySelectorAll('table.tablebg th').forEach(function (t) {
|
||
if (th) return;
|
||
var row = t.closest('tr');
|
||
if (row && row.children.length === 1) th = t;
|
||
});
|
||
var msgCell = th && th.closest('table').querySelector('td.row1');
|
||
if (!th || !msgCell) { log.push('login: kein Formular, keine Meldung'); return; }
|
||
|
||
var root = document.createElement('div');
|
||
root.className = 'rm-root rm-login';
|
||
|
||
var title = document.createElement('div');
|
||
title.className = 'rm-login-title';
|
||
title.textContent = txt(th) || 'Information';
|
||
root.appendChild(title);
|
||
|
||
msgCell.classList.add('rm-login-msg');
|
||
root.appendChild(msgCell);
|
||
|
||
replaceBody(root);
|
||
addCrumbs();
|
||
log.push('login=meldung');
|
||
}
|
||
|
||
// ==========================================================================
|
||
|
||
function run() {
|
||
try {
|
||
if (view === 'liste') buildList();
|
||
else if (view === 'thread') buildThread();
|
||
else if (view === 'login') buildLogin();
|
||
} catch (e) { log.push('build: ' + e.message); }
|
||
|
||
if (DEBUG) {
|
||
var d = document.createElement('div');
|
||
d.className = 'rm-dbg';
|
||
d.textContent = 'RIN Mobile v3.9\n' + log.join('\n');
|
||
document.body.insertBefore(d, document.body.firstChild);
|
||
}
|
||
}
|
||
|
||
if (document.readyState === 'loading')
|
||
document.addEventListener('DOMContentLoaded', run);
|
||
else run();
|
||
})();
|