您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Save your favorite threads
当前为
// ==UserScript== // @name Voz save threads // @description Save your favorite threads // @namespace Violentmonkey Scripts // @match *://voz.vn/* // @version 0.1 // @run-at document-idle // @license MIT // ==/UserScript== const wT = 10; //in ms const getSync=true; //true de tai trang lan luot, false de lay tat ca cug luc const sleep = (ms) => new Promise((rs) => setTimeout(rs, ms)); async function unZip(data) { //return Blob, lấy text thì them await .text() let blob=new Blob([data]); const ds = new DecompressionStream("gzip"); const decompressedStream = blob.stream().pipeThrough(ds); return await new Response(decompressedStream).blob(); } async function zip(data) { // return Blob let blob=new Blob([data]); const cs = new CompressionStream("gzip"); const compressedStream = blob.stream().pipeThrough(cs); return await new Response(compressedStream).blob(); } HTMLElement.prototype.directText=function (){ let el=this.cloneNode(true); while (el.children[0]) el.children[0].remove(); return el.textContent; } let threadId, dump; class IDB { constructor(...args) { //use new IDB('databaseName',['storeName1','storeName2'....]) then IDB.close()right after to create database and Stores this.db = null; if (args.length>1) this.open(...args).then(this.close()); } typeOf = v => Object.prototype.toString.call(v).slice(8,-1); async open(dbName = 'dbName', storeName = 'storeName', version = 1) { this.dbName = dbName; this.storeName = storeName; this.version = version; return new Promise((resolve, reject) => { const request = indexedDB.open(dbName, version); request.onupgradeneeded = (event) => { this.db = event.target.result; if (this.typeOf(storeName)=='Array') storeName.forEach(s=>this.db.createObjectStore(s)) else this.db.createObjectStore(storeName); resolve(true); }; request.onsuccess = (event) => { this.db = event.target.result; resolve(true); }; request.onerror = (e)=>{console.log(e); reject(e);} }); } async setItem(key, value) { return this._justDoIt('put', value, key);} async getItem(key) { return this._justDoIt('get', key);} async getAll() { return this._justDoIt('getAll');} async listAll() { return this._justDoIt('getAllKeys');} async deleteItem(key) { return this._justDoIt('delete', key);} async deleteAll() { return this._justDoIt('clear');} _justDoIt(operation, key, value) { const store=this.db.transaction(this.storeName, 'readwrite').objectStore(this.storeName); return new Promise((resolve, reject) => { const request = store[operation](key, value); request.onsuccess = () => resolve(request.result); request.onerror = reject; }); } close() { if (this.db) { this.db.close(); this.db = null; this.dbName = null; this.storeName = null; } } } function convertContent(htmlStr) { dump=new DOMParser(); let html=dump.parseFromString(htmlStr,'text/html'); html.querySelectorAll('[href]').forEach(el=> { let href=el.getAttribute('href'); if (href.startsWith('/')) el.setAttribute('href','https://voz.vn'+ href); }); html.querySelectorAll('[src]').forEach(el=> { // if(el.tagName=='SCRIPT') return; //skip script; let src=el.getAttribute('src'); if (src.startsWith('data:image')) el.setAttribute('src',el.getAttribute('data-src')); if (src.startsWith('/')) el.setAttribute('src','https://voz.vn'+ src); }); html.querySelectorAll('[srcset]').forEach(el=> { src=el.getAttribute('srcset').split(',').map(a=>{ if (a.startsWith('/')) return 'https://voz.vn'+a }).join(','); el.setAttribute('srcset',src); }); //Spoiler html.querySelectorAll('.bbCodeSpoiler-button,.bbCodeSpoiler-content').forEach(el=>el.classList.add('is-active')) // Sửa link trang html.querySelectorAll('div.pageNav a').forEach(el=>{ el.removeAttribute('href'); }); htmlStr=new XMLSerializer().serializeToString(html); return htmlStr; } dump =new IDB('VozSaveThreads',['Threads','Pages']); async function saveThread() { let threadDB = new IDB(); await threadDB.open('VozSaveThreads','Threads'); let pageDB = new IDB(); await pageDB.open('VozSaveThreads','Pages'); const maxPage = parseInt(document.querySelector("ul.pageNav-main>li:last-of-type>a").textContent); let title = document.querySelector("div.p-body-header > div.p-title > h1").directText(); let thread = await threadDB.getItem(threadId); if (!thread) thread = { threadId, title, addTime: Date.now(), maxPage: 1}; async function saveContent(pageId) { let pageUrl = `https://voz.vn/t/${threadId}/${pageId}`; console.log(pageUrl); let res= await fetch(pageUrl); const data=await res.text(); await pageDB.setItem(`${threadId}_${pageId.split('-')[1]}`, await zip(convertContent(data))); } if (maxPage >= (thread.maxPage ?? 1)) { if(getSync) { for (let i = thread.maxPage??1; i <= maxPage; i++) { let pageUrl = `https://voz.vn/t/${threadId}/page-${i}`; console.log(pageUrl); let res= await fetch(pageUrl); if(!res.ok) return false; const data=await res.text(); if (!data) return false; await pageDB.setItem(`${threadId}_${i}`, await zip(convertContent(data))); //Vi chay o content script nen o day cho dong bo thread.maxPage=i; await threadDB.setItem(threadId,thread) await sleep(wT); } } else { let run=[] for (let i = thread.maxPage??1; i <= maxPage; i++) run.push(saveContent('page-'+i)); await Promise.all(run); thread.maxPage=maxPage; await threadDB.setItem(threadId,thread); } } threadDB.close(); pageDB.close(); } (async function main() { //Create Show Saved Threads button dump = `<li><div class="p-navEl" style="cursor:pointer;"><div class="p-navEl-link">Saved Threads</div></div></li>`; document.querySelector("div.p-navSticky>nav>div>div.p-nav-scroller>div>ul").innerHTML += dump; document.querySelector("div.p-navSticky>nav>div>div.p-nav-scroller>div>ul>li>div.p-navEl>div.p-navEl-link").onclick=showSavedThreads; //create SaveThread Button dump = location.href.match(/https:\/\/(?:.*\.)?voz.vn\/(f|t)\/[a-z\d\-]+.(\d+)\/?(page-(\d+))?/); let fOrT; if (dump) { fOrT = dump[1]; threadId = dump[2]; } if (fOrT == "t") { const btnSave = document.createElement("a"); btnSave.classList.add("pageNav-jump", "pageNav-jump--next"); btnSave.textContent = "Save Thread"; btnSave.onclick = saveThread; btnSave.style = "cursor:pointer;"; document.querySelectorAll("ul.pageNav-main") .forEach((el, i) =>i == 0 ? el.parentElement.appendChild(btnSave) : ((dump = btnSave.cloneNode(true)), (dump.onclick = saveThread),el.parentElement.appendChild(dump))); } })(); async function showSavedThreads(latestFirst = true) { let html = `<html> <head> <title>Voz Saved Threads</title> </head> <body> <div id="list"></div> <div id="screen"></div> <style> :root { font-size: 1.25rem;} ul[page] { display: none; line-height: 1.7rem; } ul>input[type="radio"] {display: none;} ul:has(input[type="radio"]) {display: none;} ul:has(input[type="radio"]:checked) {display: block;} #options {border-radius: 2px; border: 1px solid black;margin: 3px;padding: 3px;} label>input[type="checkbox"] {display:none;} label:has(input[type="checkbox"])+div {display:none;} .page_number>span{cursor: pointer;} ul>li{cursor: pointer;} li>button {float:right;} .odd_line {background-color: rgb(237, 237, 235);} .even_line {background-color: rgb(251, 252, 245);} #screen {display:none;} #screen div.pageNav a{cursor: pointer;} </style> <script> class IDB { constructor(...args) { //use new IDB('databaseName',['storeName1','storeName2'....]) then IDB.close()right after to create database and Stores this.db = null; if (args.length>1) this.open(...args).then(this.close()); } typeOf = v => Object.prototype.toString.call(v).slice(8,-1); async open(dbName = 'dbName', storeName = 'storeName', version = 1) { this.dbName = dbName; this.storeName = storeName; this.version = version; return new Promise((resolve, reject) => { const request = indexedDB.open(dbName, version); request.onupgradeneeded = (event) => { this.db = event.target.result; if (this.typeOf(storeName)=='Array') storeName.forEach(s=>this.db.createObjectStore(s)) else this.db.createObjectStore(storeName); }; request.onsuccess = (event) => { this.db = event.target.result; resolve(true); }; request.onerror = (e)=>{ console.log('Request Error: ',e); reject(e); } }); } async setItem(key, value) { return this._justDoIt('put', value, key);} async getItem(key) { return this._justDoIt('get', key);} async getAll() { return this._justDoIt('getAll');} async listAll() { return this._justDoIt('getAllKeys');} async deleteItem(key) { return this._justDoIt('delete', key);} async deleteAll() { return this._justDoIt('clear');} _justDoIt(operation, key, value) { const store=this.db.transaction(this.storeName, 'readwrite').objectStore(this.storeName); return new Promise((resolve, reject) => { const request = store[operation](key, value); request.onsuccess = () => resolve(request.result); request.onerror = reject; }); } close() { if (this.db) { this.db.close(); this.db = null; this.dbName = null; this.storeName = null; } } } async function unZip(data) { //return Blob, lấy text thì them await .text() let blob=new Blob([data]); const ds = new DecompressionStream("gzip"); const decompressedStream = blob.stream().pipeThrough(ds); return await new Response(decompressedStream).blob(); } // let dump =new IDB('VozSaveThreads',['Threads','Pages']); let list = document.getElementById('list'); let screen = document.getElementById('screen'); const threadDB = new IDB(); let pageDB = new IDB(); //Create topics list async function listTopics(tpp=15) { document.querySelector('head').insertAdjacentHTML('beforeend','<title>Voz Saved Threads</title>') const threads = await threadDB.getAll(); let pageHeader='<div class="page_number"> Page: '; let ul=''; for (let i=0; i<threads.length/tpp; i++){ if(i==0) pageHeader+='<span style="font-weight:700"> '+(i+1)+' </span>'; else pageHeader+='<span> '+(i+1)+' </span>'; ul+='<ul page=' + (i+1) + '><input type="radio" name="MyNameIsRadioButton" ' + (i==0?"checked":"") + '>' for (let j=0; j<tpp && i*tpp+j<threads.length; j++) ul+='<li class="'+ (j%2==0?'even_line':'odd_line') +'"><a threadid="'+ threads[i*tpp+j].threadId + '">'+ threads[i*tpp+j].title + '</a><button threadid="'+ threads[i*tpp+j].threadId+ '">Delete</button></li>'; ul+='</ul>'; } pageHeader+='</div>' list.innerHTML=pageHeader+ul+pageHeader; /// Click page number list.querySelectorAll('div.page_number>span').forEach(span=>span.addEventListener('click',e=>{ let pageNo=e.target.textContent.trim(); list.querySelectorAll('div.page_number>span').forEach(el=> (el.textContent.trim()== pageNo) ? el.style.fontWeight='700':el.style.fontWeight='400'); list.querySelector('ul[page="'+pageNo+'"]>input[type="radio"]').click(); })) //Click topics links list.querySelectorAll('li>a').forEach(el=>{el.addEventListener('click',async (e) =>await showPage(e.target.getAttribute('threadid')))}); //Delete button list.querySelectorAll('li>button').forEach(el=>{el.addEventListener('click',async (e) =>{ let thread= await threadDB.getItem(e.target.getAttribute('threadid')); for (let i=1; i<=thread.maxPage; i++) await pageDB.deleteItem(thread.threadId+'_'+i); threadDB.deleteItem(thread.threadId); listTopics(); }) }); //show it; list.style.display='block'; screen.style.display='none'; } async function showPage(pageId) { pageId.split('_').length==1 ? pageId+='_1':pageId; let pageContent= await (await unZip(await pageDB.getItem(pageId))).text(); screen.innerHTML=pageContent; //Add "Saved Threads" on Menu dump = '<li><div class="p-navEl" style="cursor:pointer;"><div class="p-navEl-link">Saved Threads</div></div></li>'; screen.querySelector("div.p-navSticky>nav>div>div.p-nav-scroller>div>ul").innerHTML += dump; screen.querySelector("div.p-navSticky>nav>div>div.p-nav-scroller>div>ul>li>div.p-navEl>div.p-navEl-link").onclick=()=>{ screen.innerHTML=''; list.style.display='block'; screen.style.display='none'; listTopics();} //Page number click screen.querySelectorAll('ul.pageNav-main a:not([id])').forEach(el=> el.addEventListener('click',e=> { e.preventDefault(); !isNaN(e.target.innerHTML) ? showPage(pageId.split('_')[0]+'_'+e.target.textContent.trim()):'' })); //Goto page Click screen.querySelectorAll('ul.pageNav-main a[title="Go to page"]')?.forEach(el => el.addEventListener('click',e=>{ let pageNo = prompt('Enter page number','1'); if (isNaN(pageNo)) return; showPage(pageId.split('_')[0]+'_'+pageNo); }) ) //Next Click screen.querySelectorAll('.pageNav-jump.pageNav-jump--next')?.forEach(el=>el.addEventListener('click',(e)=> { const [a,b]=pageId.split('_'); showPage(a+'_'+ (parseInt(b)+1)); } )); //Next Click screen.querySelectorAll('.pageNav-jump.pageNav-jump--prev')?.forEach(el=>el.addEventListener('click',(e)=> { const [a,b]=pageId.split('_'); showPage(a+'_'+ (parseInt(b)-1)); } )); //Show it document.querySelector('title')?.remove(); list.style.display='none'; screen.style.display='block'; } (async function main() { await pageDB.open('VozSaveThreads','Pages'); await threadDB.open('VozSaveThreads','Threads'); await listTopics(); })(); </script> </body> </html>` window.open(URL.createObjectURL(new Blob([html],{type:'text/html'}),'_blank')); } //End ShowSavedThreads