// ==UserScript== // @name 摸鱼小说阅读器 Loafing-Reader // @namespace hanayabuki-loafing-reader // @version 1.5 // @description 内嵌浏览器里用来上班摸鱼看小说(可拖拽调整大小 + Alt+R 唤出) // @author HanaYabuki + ChatGPT // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @noframes // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/557709/%E6%91%B8%E9%B1%BC%E5%B0%8F%E8%AF%B4%E9%98%85%E8%AF%BB%E5%99%A8%20Loafing-Reader.user.js // @updateURL https://update.greasyfork.icu/scripts/557709/%E6%91%B8%E9%B1%BC%E5%B0%8F%E8%AF%B4%E9%98%85%E8%AF%BB%E5%99%A8%20Loafing-Reader.meta.js // ==/UserScript== (function () { const cssText = ` :root { --lf-color: #222; --lf-toolbar-background-color: #aaa3; --lf-content-background-color: #fff3; --lf-btn-color: #00a; --lf-btn-color-hover: #00f; } [lf-theme='dark'] { --lf-color: #ddd; --lf-toolbar-background-color: #5553; --lf-content-background-color: #2223; --lf-btn-color: #aa0; --lf-btn-color-hover: #ff0; } .loafing-reader { margin: 0; padding: 0; box-sizing: content-box; font-size: 12px; color: var(--lf-color); } #lf-panel { height: 27em; width: 48em; background-color: #f000; top: 50%; left: 50%; z-index: 10; position: fixed; display: flex; flex-flow: column nowrap; user-select: none; backdrop-filter: blur(1px); } #lf-toolbar { background: var(--lf-toolbar-background-color); width: 100%; height: 18px; } .lf-item { padding: 0 0 0 1em; } .lf-btn { color: var(--lf-btn-color); } .lf-btn:hover { color: var(--lf-btn-color-hover); } #lf-content { background-color: var(--lf-content-background-color); flex: 1; padding: 0 0.5em; overflow: hidden; } #lf-text { background-color: #f000; position: relative; } .lf-hidden { display: none; } #lf-trigger { position: fixed; top: 0; left: 0; width: 20px; height: 20px; background: linear-gradient(-45deg, transparent 14px, pink 0); z-index: 16777271; } #lf-resize-handle { width: 16px; height: 16px; position: absolute; right: 0; bottom: 0; cursor: se-resize; background: linear-gradient(45deg, transparent 50%, #aaa 50%); z-index: 20; } `; const elements = {}; function ce(tagName, id, children = [], ...clazz) { const tmp = document.createElement(tagName); tmp.setAttribute('id', 'lf-' + id); tmp.setAttribute('class', ['loafing-reader', ...(clazz.map(i => 'lf-' + i))].join(' ')); children.forEach(i => tmp.appendChild(i)); return elements[id] = tmp; } ce('div', 'panel', [ ce('div', 'toolbar', [ ce('input', 'fileholder', [], 'hidden'), ce('span', 'jump', [], 'item', 'btn'), ce('span', 'load', [], 'item', 'btn'), ce('span', 'move', [], 'item', 'btn'), ce('span', 'info', [], 'item'), ce('span', 'color', [], 'item', 'btn'), ]), ce('div', 'content', [ ce('div', 'text', []) ]), ]); ce('div', 'trigger', [], 'trigger'); // 初始化文字和按钮 elements.jump.innerText = '[跳转]'; elements.load.innerText = '[加载]'; elements.move.innerText = '[移动]'; elements.fileholder.type = 'file'; elements.fileholder.accept = '.txt'; elements.info.innerText = '(无文件)'; elements.color.innerText = '[主题]'; document.documentElement.appendChild(elements.panel); document.documentElement.appendChild(elements.trigger); // 添加拉伸手柄 const resizeHandle = document.createElement('div'); resizeHandle.id = 'lf-resize-handle'; elements.panel.appendChild(resizeHandle); // 文件信息 const fileInfo = {}; function loadFile(filename, content) { clear(); fileInfo.fileName = filename.substring(0, filename.lastIndexOf('.')); fileInfo.content = content.split(/(?:\r\n|\n)/); fileInfo.length = fileInfo.content.length; fileInfo.bookmark = 0; fileInfo.page = []; GM_setValue('lf_file_name', filename); GM_setValue('lf_file_content', content); GM_setValue('lf_bookmark', 0); jump(0); } // CSS GM_addStyle(cssText); // 主题切换 const themes = ['light', 'dark']; let themeId = 1; elements.color.addEventListener('click', function () { elements.panel.setAttribute('lf-theme', themes.at(themeId)); themeId = (themeId + 1) % themes.length; }); // 工具函数 function updateInfo() { const filename = fileInfo.fileName || ''; elements.info.innerText = `(${fileInfo.bookmark}/${fileInfo.length})-${filename}`; GM_setValue('lf_bookmark', fileInfo.bookmark); } function clear() { const ls = fileInfo.page; while (ls && ls.length > 0) { ls.pop()?.remove(); } } function render(mark, removeNumber, direction) { const ls = fileInfo.page; for (let i = 0; i < removeNumber; ++i) { if (direction) { ls.shift()?.remove(); } else { ls.pop()?.remove(); } } let i = mark; while (i < fileInfo.length && i >= 0 && elements.text.offsetHeight < elements.content.offsetHeight) { const p = ce('div'); p.innerHTML = fileInfo.content[i] + ' '; if (direction) { elements.text.appendChild(p); ls.push(p); i++; } else { elements.text.insertBefore(p, elements.text.firstChild); ls.unshift(p); i--; } } return direction ? mark : i + 1; } function jump(index) { render(index, fileInfo.page.length, true); fileInfo.bookmark = index; updateInfo(); } function next() { const ls = fileInfo.page; if (fileInfo.bookmark + 1 >= fileInfo.length || ls.length === 0) { alert('已是最后一页'); return; } let i = fileInfo.bookmark + ls.length; const s = Math.max(ls.length - 1, 1); render(i, s, true); fileInfo.bookmark += s; updateInfo(); } function previous() { const ls = fileInfo.page; if (fileInfo.bookmark === 0 || ls.length === 0) { alert('已经是第一页'); return; } const i = fileInfo.bookmark; const mk = render(i, ls.length, false); fileInfo.bookmark = mk; updateInfo(); } // 事件 elements.jump.addEventListener('click', () => { let value = parseInt(prompt('跳转到?', fileInfo.bookmark)); if (!isNaN(value) && fileInfo.content && fileInfo.length >= value) { jump(value); } else { alert('输入有误,跳转失败'); } }); let charset = "utf-8"; elements.fileholder.addEventListener('change', function () { const file = elements.fileholder.files[0]; const reader = new FileReader(); reader.readAsText(file, charset); reader.onload = function () { loadFile(file.name, this.result); } }); elements.load.addEventListener('click', function () { charset = prompt("选择文件编码格式", charset) elements.fileholder.click(); }); elements.content.addEventListener('contextmenu', e => e.preventDefault()); elements.content.addEventListener('mousedown', e => { if (e.button === 0) next(); else if (e.button === 2) previous(); }); // 面板移动 let mouseMemory = [0, 0]; let moveWindow = false; elements.move.addEventListener('mousedown', function (e) { mouseMemory = [e.clientX - mouseMemory[0], e.clientY - mouseMemory[1]]; moveWindow = !moveWindow; }); document.documentElement.addEventListener('mousemove', function (e) { if (moveWindow) { elements.panel.style.left = `calc(50% + ${e.clientX - mouseMemory[0]}px)`; elements.panel.style.top = `calc(50% + ${e.clientY - mouseMemory[1]}px)`; } }); // 面板拖拽调整大小 let resizing = false; let startMouse = [0, 0]; let startSize = [0, 0]; resizeHandle.addEventListener('mousedown', function (e) { e.stopPropagation(); resizing = true; startMouse = [e.clientX, e.clientY]; startSize = [elements.panel.offsetWidth, elements.panel.offsetHeight]; }); document.addEventListener('mousemove', function (e) { if (resizing) { const dx = e.clientX - startMouse[0]; const dy = e.clientY - startMouse[1]; elements.panel.style.width = startSize[0] + dx + 'px'; elements.panel.style.height = startSize[1] + dy + 'px'; } }); document.addEventListener('mouseup', function () { if (resizing) resizing = false; }); // 唤醒和隐藏 elements.panel.style.visibility = 'hidden'; elements.trigger.addEventListener('click', wakeUp); elements.panel.addEventListener('mouseleave', sleepDown); // Alt+R 唤醒 document.addEventListener('keydown', function(event) { if (event.altKey && event.key.toLowerCase() === 'r') { event.preventDefault(); wakeUp(); } }); function wakeUp() { elements.panel.style.visibility = 'visible'; if (!window.LOAFING_READER_INIT) { init(); window.LOAFING_READER_INIT = true; console.log('loafing-reader loaded.') } const bookmark = GM_getValue('lf_bookmark'); if (bookmark !== fileInfo.bookmark) jump(bookmark); } function sleepDown() { if (!moveWindow) elements.panel.style.visibility = 'hidden'; } // 初始化 window.LOAFING_READER_INIT = false; function init() { const lfFileName = GM_getValue('lf_file_name'); const lfFileContent = GM_getValue('lf_file_content'); const lfBookmark = GM_getValue('lf_bookmark', 0); if (lfFileName && lfFileContent) loadFile(lfFileName, lfFileContent); if (fileInfo.content) jump(lfBookmark); } })();