mirror of
https://github.com/Andre-Bzk/rin-mobile-userscript.git
synced 2026-11-15 15:43:07 +01:00
feat: add first, last and direct page jump to thread navigation
Threads on this forum reach four-digit page counts, and stepping one page at a time was the only movement the navigation bar offered. The end of a thread, or any specific page, was effectively unreachable on a phone. Jump targets are derived arithmetically from the forum's own page size instead of read from the rendered pagination, because phpBB never renders more than a handful of page links regardless of thread length. Where that page size cannot be determined, the bar falls back to the links the forum itself provides and hides the jump. Both userscripts inject their stylesheet on thread pages, so class names are a shared namespace between them. The new ones avoid the two the list view already uses, which would otherwise collide at equal specificity and resolve by load order. Claude-Session: https://claude.ai/code/session_0158owAAsXAfFiT3hGkQt1Xk
This commit is contained in:
@@ -15,6 +15,14 @@ existing page content is hidden and a freshly built DOM is appended to `<body>`.
|
|||||||
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Topic lists (`viewforum.php`, `search.php`) as cards |
|
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Topic lists (`viewforum.php`, `search.php`) as cards |
|
||||||
| `rin-thread.user.js` | `*://cs.rin.ru/forum/viewtopic.php*` | Single thread as a post stream with a fixed bottom navigation bar |
|
| `rin-thread.user.js` | `*://cs.rin.ru/forum/viewtopic.php*` | Single thread as a post stream with a fixed bottom navigation bar |
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
Both scripts run at `document-start`, inject a mobile viewport meta tag, and
|
Both scripts run at `document-start`, inject a mobile viewport meta tag, and
|
||||||
carry a `DEBUG` flag. While `DEBUG` is `true`, a green diagnostic overlay is
|
carry a `DEBUG` flag. While `DEBUG` is `true`, a green diagnostic overlay is
|
||||||
prepended to the page showing how many rows were found and converted. Set it to
|
prepended to the page showing how many rows were found and converted. Set it to
|
||||||
@@ -164,6 +172,15 @@ ein neu gebautes DOM an `<body>` gehängt.
|
|||||||
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Themenlisten (`viewforum.php`, `search.php`) als Karten |
|
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Themenlisten (`viewforum.php`, `search.php`) als Karten |
|
||||||
| `rin-thread.user.js` | `*://cs.rin.ru/forum/viewtopic.php*` | Einzelner Thread als Beitragsstrom mit fixierter Navigationsleiste |
|
| `rin-thread.user.js` | `*://cs.rin.ru/forum/viewtopic.php*` | Einzelner Thread als Beitragsstrom mit fixierter Navigationsleiste |
|
||||||
|
|
||||||
|
Die Navigationsleiste im Thread enthält erste / vorherige / nächste / letzte
|
||||||
|
Seite und eine Positionsanzeige. Ein 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.
|
||||||
|
|
||||||
Beide laufen bei `document-start`, setzen ein mobiles Viewport-Meta-Tag und
|
Beide laufen bei `document-start`, setzen ein mobiles Viewport-Meta-Tag und
|
||||||
haben ein `DEBUG`-Flag. Solange `DEBUG` auf `true` steht, wird ein grünes
|
haben ein `DEBUG`-Flag. Solange `DEBUG` auf `true` steht, wird ein grünes
|
||||||
Diagnose-Overlay über die Seite gelegt, das zeigt, wie viele Zeilen gefunden und
|
Diagnose-Overlay über die Seite gelegt, das zeigt, wie viele Zeilen gefunden und
|
||||||
|
|||||||
+241
-17
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name RIN Mobile Thread
|
// @name RIN Mobile Thread
|
||||||
// @description Mobile Beitragsansicht fuer cs.rin.ru
|
// @description Mobile Beitragsansicht fuer cs.rin.ru
|
||||||
// @version 2.2
|
// @version 2.3
|
||||||
// @match *://cs.rin.ru/forum/viewtopic.php*
|
// @match *://cs.rin.ru/forum/viewtopic.php*
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
@@ -93,8 +93,55 @@
|
|||||||
white-space:nowrap; }
|
white-space:nowrap; }
|
||||||
.rm-nav a { padding:7px 13px; border-radius:99px;
|
.rm-nav a { padding:7px 13px; border-radius:99px;
|
||||||
border:1px solid rgba(160,160,160,.45); }
|
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 { 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-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);
|
h().appendChild(st);
|
||||||
|
|
||||||
@@ -116,6 +163,174 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 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 m = (el.textContent || '').match(/per_page\s*=\s*'?(\d+)/);
|
||||||
|
if (m) s.per = parseInt(m[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' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function build() {
|
function build() {
|
||||||
var bodies = document.querySelectorAll('span.postbody, .postbody');
|
var bodies = document.querySelectorAll('span.postbody, .postbody');
|
||||||
log.push('postbodies=' + bodies.length);
|
log.push('postbodies=' + bodies.length);
|
||||||
@@ -256,28 +471,37 @@
|
|||||||
if (!built) return;
|
if (!built) return;
|
||||||
|
|
||||||
// --- Navigationsleiste ---
|
// --- Navigationsleiste ---
|
||||||
|
var pg = pageState();
|
||||||
var nav = document.createElement('div');
|
var nav = document.createElement('div');
|
||||||
nav.className = 'rm-nav';
|
nav.className = 'rm-nav';
|
||||||
var prevA = null, nextA = null;
|
|
||||||
document.querySelectorAll('a').forEach(function (a) {
|
|
||||||
var t = txt(a);
|
|
||||||
if (!prevA && /^(Previous|Vorherige)$/i.test(t)) prevA = a;
|
|
||||||
if (!nextA && /^(Next|Weiter|Nächste)$/i.test(t)) nextA = a;
|
|
||||||
});
|
|
||||||
var pos = txt(document.body).match(/Page\s+(\d+)\s+of\s+(\d+)/i);
|
|
||||||
|
|
||||||
function navBtn(a, label) {
|
function navBtn(href, label, aria) {
|
||||||
var e = document.createElement('a');
|
var e = document.createElement('a');
|
||||||
|
e.className = 'rm-step' + (href ? '' : ' rm-off');
|
||||||
e.textContent = label;
|
e.textContent = label;
|
||||||
if (a) e.href = a.href; else e.className = 'rm-off';
|
e.setAttribute('aria-label', aria);
|
||||||
|
if (href) e.href = href;
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
nav.appendChild(navBtn(prevA, '‹ Zurück'));
|
nav.appendChild(navBtn(pg.url(1), '«', 'Erste Seite'));
|
||||||
var ps = document.createElement('span');
|
nav.appendChild(navBtn(pg.prev, '‹', 'Vorherige Seite'));
|
||||||
ps.className = 'rm-pos';
|
|
||||||
ps.textContent = pos ? pos[1] + ' / ' + pos[2] : '';
|
var pos = document.createElement('span');
|
||||||
nav.appendChild(ps);
|
pos.className = 'rm-pos';
|
||||||
nav.appendChild(navBtn(nextA, 'Weiter ›'));
|
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'));
|
||||||
root.appendChild(nav);
|
root.appendChild(nav);
|
||||||
|
|
||||||
Array.prototype.forEach.call(document.body.children, function (el) {
|
Array.prototype.forEach.call(document.body.children, function (el) {
|
||||||
@@ -291,7 +515,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 Thread v2.2\n' + log.join('\n');
|
d.textContent = 'RIN Thread v2.3\n' + log.join('\n');
|
||||||
document.body.insertBefore(d, document.body.firstChild);
|
document.body.insertBefore(d, document.body.firstChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user