Greasy Fork is available in English.
RisuRealm의 긴 텍스트 넘침을 수정하고 페이지 이동 및 오른쪽 고정 스크롤 버튼을 추가합니다.
// ==UserScript==
// @name RisuRealm UI Plus
// @namespace https://realm.risuai.net/
// @version 1.0.9
// @license MIT
// @description Fix long text overflow and add quick page jump controls to RisuRealm.
// @description:en Fix long text overflow and add quick page jump controls to RisuRealm.
// @description:zh-CN 修复 RisuRealm 长文本溢出,并增强分页跳转与右侧悬浮滚动按钮。
// @description:ko RisuRealm의 긴 텍스트 넘침을 수정하고 페이지 이동 및 오른쪽 고정 스크롤 버튼을 추가합니다.
// @description:ja RisuRealm の長いテキストのはみ出しを修正し、ページ移動と右側固定スクロールボタンを追加します。
// @match https://realm.risuai.net/*
// @icon https://realm.risuai.net/favicon.ico
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
window.__RisuRealmUiPlus = true;
const css = `
html,
body {
max-width: 100% !important;
overflow-x: hidden !important;
overflow-x: clip !important;
}
body :where(p, li, a, pre, code),
body :where(article, section, div, span):not(:has(img, video, canvas, svg)) {
overflow-wrap: anywhere !important;
word-break: break-word;
}
body :where(pre, code) {
white-space: pre-wrap !important;
}
.rrp-pager {
display: none !important;
}
.rrui-native-hidden {
display: none !important;
}
.rrui-pager {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin: 16px auto 10px;
max-width: min(100%, 720px);
padding: 0 12px;
}
.rrui-pager button,
.rrui-pager input {
background: #e5e7eb;
border: 1px solid rgba(17, 24, 39, 0.12);
border-radius: 999px;
box-sizing: border-box;
color: #1f2937;
flex: 0 0 auto;
font: 600 13px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
height: 36px;
white-space: nowrap;
}
.rrui-pager button {
cursor: pointer;
min-width: 36px;
padding: 0 13px;
}
.rrui-pager button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.rrui-pager .rrui-page-current:disabled {
background: #f43f5e;
border-color: #fb7185;
color: #fff;
opacity: 1;
}
.rrui-pager input {
max-width: 120px;
min-width: 42px;
padding: 0 10px;
text-align: center;
width: auto;
}
.rrui-pager input::-webkit-inner-spin-button,
.rrui-pager input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.rrui-page-label {
color: rgba(17, 24, 39, 0.72);
font: 600 13px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.rrui-jump-row {
align-items: center;
display: flex;
flex: 0 0 100%;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-top: 2px;
}
.rrui-scroll-controls {
bottom: 92px;
display: flex;
flex-direction: column;
gap: 8px;
position: fixed;
right: 16px;
z-index: 9999;
}
.rrui-scroll-controls button {
align-items: center;
background: #e5e7eb;
border: 1px solid rgba(17, 24, 39, 0.14);
border-radius: 999px;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.16);
color: #374151;
cursor: pointer;
display: flex;
font: 800 18px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
height: 44px;
justify-content: center;
min-width: 44px;
padding: 0;
width: 44px;
}
.rrui-scroll-controls button[hidden],
.rrui-scroll-controls[hidden] {
display: none !important;
}
@media (max-width: 640px) {
.rrui-scroll-controls {
bottom: 82px;
right: 10px;
}
}
`;
let pager;
let scrollControls;
let scheduled = false;
let scrollScheduled = false;
const FILTER_PARAMS = ['sort', 'mode', 'nsfw'];
addStyle(css);
repairCurrentUrl();
ready(() => {
render();
ensureScrollControls();
updateScrollControls();
observe();
patchHistory();
document.addEventListener('click', normalizeLinkNavigation, true);
window.addEventListener('scroll', scheduleScrollControls, { passive: true });
window.addEventListener('resize', scheduleScrollControls);
window.addEventListener('popstate', scheduleRender);
});
function ready(callback) {
if (document.body) {
callback();
return;
}
document.addEventListener('DOMContentLoaded', callback, { once: true });
}
function render() {
const nativePager = findNativePager();
if (!nativePager || !isListPage()) {
if (pager) pager.remove();
pager = null;
document.querySelectorAll('.rrui-native-hidden').forEach((node) => node.classList.remove('rrui-native-hidden'));
return;
}
nativePager.classList.add('rrui-native-hidden');
if (!pager) {
pager = document.createElement('form');
pager.className = 'rrui-pager';
pager.addEventListener('submit', (event) => {
event.preventDefault();
const inputNode = pager.querySelector('input');
goToPage(Number(inputNode && inputNode.value));
});
nativePager.insertAdjacentElement('afterend', pager);
} else if (pager.previousElementSibling !== nativePager) {
nativePager.insertAdjacentElement('afterend', pager);
}
const page = currentPage();
if (pager.dataset.rruiPage === String(page)) return;
pager.dataset.rruiPage = String(page);
pager.innerHTML = '';
pager.append(
button('首页', () => goToPage(1), page <= 1),
button('-10', () => goToPage(page - 10), page <= 1),
button('上一页', () => goToPage(page - 1), page <= 1),
...pageButtons(page),
button('下一页', () => goToPage(page + 1)),
button('+10', () => goToPage(page + 10)),
jumpRow(page)
);
}
function pageButtons(page) {
const nodes = [];
const start = Math.max(1, page - 2);
const end = page + 2;
if (start > 1) {
nodes.push(label('...'));
}
for (let n = start; n <= end; n += 1) {
const node = button(String(n), () => goToPage(n), n === page);
if (n === page) node.classList.add('rrui-page-current');
nodes.push(node);
}
nodes.push(label('...'));
return nodes;
}
function jumpRow(page) {
const node = document.createElement('div');
node.className = 'rrui-jump-row';
node.append(
label('第'),
input(page),
label('页'),
button('跳转', () => goToPage(Number(node.querySelector('input').value)))
);
return node;
}
function findNativePager() {
return Array.from(document.querySelectorAll('div.mt-4.w-full.flex.justify-center.items-center'))
.find((node) => node.querySelectorAll('button').length >= 2 && /^\d+$/.test((node.textContent || '').trim()));
}
function isListPage() {
return location.pathname === '/' || location.pathname === '';
}
function isListPath(pathname) {
return pathname === '/' || pathname === '';
}
function currentPage() {
const page = Number(new URL(location.href).searchParams.get('page') || 1);
return Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
}
function goToPage(page) {
if (!Number.isFinite(page)) return;
const nextPage = Math.max(1, Math.floor(page));
const url = new URL(location.href);
if (!url.searchParams.has('sort')) url.searchParams.set('sort', '');
url.searchParams.set('page', String(nextPage));
normalizeListQuery(url);
location.href = url.href;
}
function normalizeListUrl(rawUrl) {
if (rawUrl == null) return rawUrl;
let url;
try {
url = new URL(rawUrl, location.href);
} catch (error) {
return rawUrl;
}
if (url.origin !== location.origin || !isListPath(url.pathname)) return rawUrl;
const current = new URL(location.href);
const filterChanged = FILTER_PARAMS.some((key) => (current.searchParams.get(key) || '') !== (url.searchParams.get(key) || ''));
if (filterChanged) url.searchParams.set('page', '1');
if (url.searchParams.has('page') && !url.searchParams.has('sort')) url.searchParams.set('sort', '');
normalizeListQuery(url);
return url.href;
}
function normalizeListQuery(url) {
const page = Number(url.searchParams.get('page') || 1);
if ((url.searchParams.get('sort') || '') === 'recommended' && Number.isFinite(page) && page > 1) {
url.searchParams.set('sort', '');
}
const original = new URLSearchParams(url.search);
const ordered = new URLSearchParams();
['sort', 'mode', 'page', 'nsfw'].forEach((key) => {
if (original.has(key)) ordered.set(key, original.get(key) || '');
});
original.forEach((value, key) => {
if (!['sort', 'mode', 'page', 'nsfw'].includes(key)) ordered.append(key, value);
});
url.search = ordered.toString();
}
function repairCurrentUrl() {
const nextUrl = normalizeListUrl(location.href);
if (nextUrl !== location.href) location.replace(nextUrl);
}
function normalizeLinkNavigation(event) {
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const target = event.target && (event.target.nodeType === 1 ? event.target : event.target.parentElement);
const link = target && target.closest ? target.closest('a[href]') : null;
if (!link) return;
const nextUrl = normalizeListUrl(link.getAttribute('href'));
if (nextUrl === link.href || nextUrl === link.getAttribute('href')) return;
event.preventDefault();
location.href = nextUrl;
}
function button(text, onClick, disabled) {
const node = document.createElement('button');
node.type = 'button';
node.textContent = text;
node.disabled = Boolean(disabled);
node.addEventListener('click', onClick);
return node;
}
function input(page) {
const node = document.createElement('input');
node.type = 'number';
node.min = '1';
node.step = '1';
node.inputMode = 'numeric';
node.value = String(page);
node.setAttribute('aria-label', '页码');
resizePageInput(node);
node.addEventListener('input', () => resizePageInput(node));
return node;
}
function resizePageInput(node) {
const digits = Math.max(1, String(node.value || '').length);
node.style.width = `${Math.min(120, Math.max(42, 24 + digits * 9))}px`;
}
function label(text) {
const node = document.createElement('span');
node.className = 'rrui-page-label';
node.textContent = text;
return node;
}
function ensureScrollControls() {
if (scrollControls) return;
scrollControls = document.createElement('div');
scrollControls.className = 'rrui-scroll-controls';
scrollControls.append(
scrollButton('↑', '回到顶部', () => scrollToEdge('top')),
scrollButton('↓', '回到底部', () => scrollToEdge('bottom'))
);
document.body.append(scrollControls);
}
function scrollButton(text, title, onClick) {
const node = button(text, onClick);
node.title = title;
node.setAttribute('aria-label', title);
return node;
}
function scrollToEdge(edge) {
const height = Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0);
window.scrollTo({ top: edge === 'top' ? 0 : height, behavior: 'smooth' });
}
function scheduleScrollControls() {
if (scrollScheduled) return;
scrollScheduled = true;
requestAnimationFrame(() => {
scrollScheduled = false;
updateScrollControls();
});
}
function updateScrollControls() {
if (!scrollControls) return;
const scrollTop = window.scrollY || document.documentElement.scrollTop || 0;
const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
const up = scrollControls.children[0];
const down = scrollControls.children[1];
scrollControls.hidden = maxScroll <= 8;
up.hidden = scrollTop <= 8;
down.hidden = scrollTop >= maxScroll - 8;
}
function scheduleRender() {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
render();
scheduleScrollControls();
});
}
function observe() {
new MutationObserver(() => {
scheduleRender();
scheduleScrollControls();
}).observe(document.body, { childList: true, subtree: true });
}
function patchHistory() {
['pushState', 'replaceState'].forEach((method) => {
const original = history[method];
history[method] = function patchedHistoryMethod() {
if (arguments.length > 2) arguments[2] = normalizeListUrl(arguments[2]);
const result = original.apply(this, arguments);
scheduleRender();
return result;
};
});
}
function addStyle(text) {
const style = document.createElement('style');
style.textContent = text;
(document.head || document.documentElement).append(style);
}
})();