您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动保存 InPageEdit 在每个页面的上一次编辑内容到本地,也支持保存默认编辑器的内容
// ==UserScript== // @name IPE自动保存 (on THBWiki) // @namespace https://greasyfork.org/users/551710 // @version 1.0 // @description 自动保存 InPageEdit 在每个页面的上一次编辑内容到本地,也支持保存默认编辑器的内容 // @author Gzz // @match *://thwiki.cc/* // @match *://touhou.review/* // @icon https://static.thbwiki.cc/favicon.ico // @license MIT // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // ==/UserScript== (function() { const style = document.createElement('style'); style.innerHTML = ` .ipe-autosave-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); overflow: auto; z-index: 9961; } .ipe-autosave-content { background: white; border-radius: 5px; width: fit-content; margin: 40px auto; padding: 30px 20px 20px; position: relative; outline: none; } .ipe-autosave-content button, .ipe-autosave-toolbar button { font-size: 14px; font-weight: bold; color: #222; border: 1px solid #c8ccd1; border-radius: 2px; padding: 0.2em 0.6em; background-color: #f8f9fa; } .ipe-autosave-content button:hover, .ipe-autosave-toolbar button:hover { background-color: #ffffff; color: #454545; } .ipe-autosave-content button:active, .ipe-autosave-toolbar button:active { border: 1px solid #36c; } .ipe-autosave-content button:focus, .ipe-autosave-toolbar button:focus { box-shadow: inset 0 0 0 1px #36c; } .ipe-autosave-close { position: absolute; top: 5px; right: 10px; cursor: pointer; font-weight: bold; } .ipe-autosave-close::after { content: '×'; } .ipe-autosave-body { display: grid; grid-template-columns: repeat(6, auto); align-items: center; gap: 5px; } .ipe-autosave-body > :nth-child(6n+1) { justify-self: end; } .ipe-autosave-body > a { width: fit-content; max-width: 300px; } .ipe-autosave-body > .mark-btn { font-size: 32px; font-family: "Arial Unicode MS", "Segoe UI Symbol", sans-serif; color: #666; line-height: 0.5; } .ipe-autosave-body > .mark-btn::after { content: "☆"; position: relative; top: -2px; transition: color 0.1s; } .ipe-autosave-body > .mark-btn:hover::after { color: #FFB74D; } .ipe-autosave-body > .mark-btn.marked::after { content: "★"; color: #FFB74D; } .ipe-autosave-footer > :first-child { margin-top: 10px; } .ipe-autosave-toolbar { display: flex; align-items: center; margin-top: 5px; } .ipe-autosave-toolbar > button { margin-left: 40px; } .ipe-autosave-toolbar > button:last-of-type { margin-left: auto; } .ipe-autosave-toolbar.right { float: right; } .ipe-autosave-toolbar.right > * { margin-right: 10px; } .ipe-autosave-toolbar.right > button { font-size: 16px; padding: 0.25em 0.8em; } `; document.head.appendChild(style); // 用于存放已经处理过的编辑器 const handledEditors = new WeakSet(); // 保存内容的天数 let expiry = GM_getValue('autosave_expiry', 7); // 注册脚本菜单项 GM_registerMenuCommand('查看已保存的页面', () => { if (document.getElementById('ipe-autosave')) return; function generateList() { body.innerHTML = footer.innerHTML = ''; const list = GM_getValue('autosave_list', []); list.reverse().sort((a, b) => b.marked - a.marked); let sizeTotal = 0; list.forEach((item, index) => { const title = item.title; const text = GM_getValue(title); const number = document.createElement('span'); number.textContent = (index + 1) + '.'; const link = document.createElement('a'); link.href = '/' + title; link.textContent = title; link.target = '_blank'; const mark = document.createElement('a'); mark.className = item.marked ? 'mark-btn marked' : 'mark-btn'; mark.title = '收藏后会禁止自动删除'; const time = document.createElement('span'); time.textContent = new Date(item.time).toLocaleString(); const size = document.createElement('span'); const length = byteLength(text); size.textContent = `(${length.toLocaleString()} 字节)`; sizeTotal += length; const button = copyButton(title, text); body.append(number, link, mark, time, size, button); mark.addEventListener('click', () => { const marked = mark.classList.toggle('marked'); const list = GM_getValue('autosave_list', []); const obj = list.find(i => i.title === title); if (obj) obj.marked = marked; GM_setValue('autosave_list', list); log(`已${marked ? '' : '取消'}收藏`, title); }); }); const divSize = document.createElement('div'); divSize.textContent = `总大小: ${(sizeTotal / 1024).toFixed(2)} KB (${(sizeTotal / 1024 ** 2).toFixed(2)} MB)`; const pageExpiry = document.createElement('span'); pageExpiry.textContent = '当前保存天数: ' + expiry; const changeExpiry = document.createElement('button'); changeExpiry.textContent = '更改天数'; const clearAll = document.createElement('button'); clearAll.textContent = '清空全部'; const toolbar = document.createElement('div'); toolbar.className = 'ipe-autosave-toolbar'; toolbar.append(pageExpiry, changeExpiry, clearAll); footer.append(divSize, toolbar); changeExpiry.addEventListener('click', () => { const input = prompt('将保存天数更改为', expiry); if (/^\d+$/.test(input)) { expiry = input; GM_setValue('autosave_expiry', expiry); log('保存天数已改为', expiry); saveContent(); generateList(); } else if (input !== null) { alert('请输入非负整数'); } }); clearAll.addEventListener('click', () => { const result = confirm('是否确认清空已保存的页面?'); if (result) { const temp = expiry; expiry = 0; saveContent(); expiry = temp; generateList(); log('已清空全部页面'); } }); } const overlay = document.createElement('div'); overlay.className = 'ipe-autosave-overlay'; overlay.id = 'ipe-autosave'; const content = document.createElement('div'); content.className = 'ipe-autosave-content'; content.tabIndex = '0'; const close = document.createElement('span'); close.className = 'ipe-autosave-close'; const body = document.createElement('div'); body.className = 'ipe-autosave-body'; const footer = document.createElement('div'); footer.className = 'ipe-autosave-footer'; content.append(close, body, footer); overlay.append(content); generateList(); document.body.append(overlay); document.body.style.overflow = 'hidden'; content.focus(); log('已打开保存的页面列表'); function closeModal() { overlay.remove(); document.body.style.overflow = ''; log('已关闭页面列表'); } close.addEventListener('click', closeModal); // 点击遮罩也可以关闭 overlay.addEventListener('click', (event) => { if (event.target === overlay) closeModal(); }); }); // 函数: 给 log 加前缀 function log(...args) { console.log('[IPE自动保存]', ...args); } // 函数: 计算 utf-8 编码下的字节数 function byteLength(str) { return new TextEncoder().encode(str).length; } // 函数: 创建复制按钮 function copyButton(title, text) { const button = document.createElement('button'); button.textContent = '复制内容'; let timer = null; button.addEventListener('click', async () => { await navigator.clipboard.writeText(text); button.textContent = '复制成功'; log('复制成功:', title); clearTimeout(timer); timer = setTimeout(() => { button.textContent = "复制内容"; }, 5000); }); return button; } // 函数: 保存内容 function saveContent(title, text, span) { // 已保存页面的列表 let list = GM_getValue('autosave_list', []); const time = Date.now(); if (title) { GM_setValue(title, text); span.textContent = new Date(time).toLocaleString() + ' 已保存.'; log('已储存', title); // 将页面加入列表 const marked = list.find(i => i.title === title)?.marked === true; list = list.filter(i => i.title !== title); list.push({ title: title, time: time, marked: marked }); } // 清理超过保存天数的内容 const date = time - expiry * 86400 * 1000; const kept = [], expired = []; list.forEach(item => { if (item.time > date || item.marked) { kept.push(item); } else { expired.push(item); } }); expired.forEach(item => { GM_deleteValue(item.title); log('已清理:', item); }); GM_setValue('autosave_list', kept); } // 函数: 启用自动保存 async function startAutoSave(editor, ipe = true) { const title = editor.querySelector(ipe ? '.editPage' : '#firstHeadingTitle')?.innerText; log('发现新编辑器:', title); // 创建容器 const spanThis = document.createElement('span'); const toolbar = document.createElement('div'); toolbar.className = 'ipe-autosave-toolbar right'; toolbar.append(spanThis); editor.querySelector(ipe ? '#ssi-buttons' : '.editButtons').prepend(toolbar); // 读取上次保存的内容 const list = GM_getValue('autosave_list', []); const obj = list.find(i => i.title === title); if (obj) { const time = new Date(obj.time).toLocaleString(); const oldText = GM_getValue(title); // 创建按钮和文本 const button = copyButton(title, oldText); button.title = '复制上次编辑时自动保存到本地的内容'; button.type = 'button'; const spanLast = document.createElement('span'); spanLast.textContent = '上次编辑: ' + time; toolbar.append(spanLast, button); } let textarea, textInitial; if (ipe) { // 等待初始文本填充 textarea = editor.querySelector('textarea.editArea'); textInitial = await new Promise(resolve => { const timer = setInterval(() => { const text = textarea?.value; if (text) { clearInterval(timer); resolve(text); } }, 50); }); // 防止未保存时退出网页 window.addEventListener('beforeunload', (event) => { const saving = document.querySelector('.in-page-edit.loadingbox'); if (textarea.value !== textInitial && document.body.contains(editor) && !saving) { event.preventDefault(); event.returnValue = ''; } }); } else { textarea = editor.querySelector('#wpTextbox1'); textInitial = textarea.value; } // 每 5 秒检查一次 let textLast = null; const timer = setInterval(() => { // 如果编辑器关闭, 停止定时保存 if (!document.body.contains(editor)) { clearInterval(timer); handledEditors.delete(editor); log('编辑器已关闭, 停止保存', title); return; } // 与初始文本不同才会保存 const text = textarea.value; if (text !== textInitial && text !== textLast) { saveContent(title, text, spanThis); textLast = text; } // 未保存退出时提示 textarea.dataset.modifiled = text !== textInitial; }, 5000); } // 查找默认编辑器 if (document.getElementById('wpTextbox1')) { startAutoSave(document.getElementById('content'), false); } // 监听 <body> 直接子元素变化 const observer = new MutationObserver(() => { document.querySelectorAll('.in-page-edit.ipe-editor').forEach(editor => { if (!handledEditors.has(editor)) { handledEditors.add(editor); startAutoSave(editor); } }); }); observer.observe(document.body, { childList: true }); })();