diff --git a/README.md b/README.md index 4935967..a5440b5 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,14 @@ existing page content is hidden and a freshly built DOM is appended to ``. | `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 | +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 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 @@ -164,6 +172,15 @@ ein neu gebautes DOM an `` gehängt. | `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 | +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 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 diff --git a/rin-thread.user.js b/rin-thread.user.js index ac6fe02..a7c7a6f 100644 --- a/rin-thread.user.js +++ b/rin-thread.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name RIN Mobile Thread // @description Mobile Beitragsansicht fuer cs.rin.ru -// @version 2.2 +// @version 2.3 // @match *://cs.rin.ru/forum/viewtopic.php* // @run-at document-start // ==/UserScript== @@ -93,8 +93,55 @@ 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); @@ -116,6 +163,174 @@ 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() { var bodies = document.querySelectorAll('span.postbody, .postbody'); log.push('postbodies=' + bodies.length); @@ -256,28 +471,37 @@ if (!built) return; // --- Navigationsleiste --- + var pg = pageState(); var nav = document.createElement('div'); 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'); + e.className = 'rm-step' + (href ? '' : ' rm-off'); 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; } - nav.appendChild(navBtn(prevA, '‹ Zurück')); - var ps = document.createElement('span'); - ps.className = 'rm-pos'; - ps.textContent = pos ? pos[1] + ' / ' + pos[2] : ''; - nav.appendChild(ps); - nav.appendChild(navBtn(nextA, 'Weiter ›')); + 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')); root.appendChild(nav); Array.prototype.forEach.call(document.body.children, function (el) { @@ -291,7 +515,7 @@ if (DEBUG) { var d = document.createElement('div'); 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); } }