您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
绅士仓库历史记录可视化.
// ==UserScript== // @name 绅士仓库历史记录可视化 // @name:en CangkuHistoryRecord // @description 绅士仓库历史记录可视化. // @description:en 绅士仓库历史记录可视化. // @author Yiero // @version 2.5.2 // @match https://cangku.moe/* // @icon https://cangku.moe/favicon.ico // @namespace https://github.com/AliubYiero/TamperMonkeyScripts/ // @license GPL // @run-at document-idle // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_addElement // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // ==/UserScript== const globalConfig = { // 已看帖子透明度 opacity: 0.3, // 按钮显示 (true表示显示按钮, false表示关闭按钮) displayBtn: true, // 获取到元素之后的相应延时 getElementDelayPerSecond: 0.5 }; var __defProp = Object.defineProperty; var __defNormalProp = ( obj, key, value ) => key in obj ? __defProp( obj, key, { enumerable: true, configurable: true, writable: true, value: value } ) : obj[key] = value; var __publicField = ( obj, key, value ) => { __defNormalProp( obj, typeof key !== "symbol" ? key + "" : key, value ); return value; }; class GMStorage { constructor( key ) { __publicField( this, "key" ); this.key = key; } set( value ) { dispatchEvent( new CustomEvent( "GMStorageUpdate", { detail: { newValue: value, oldValue: this.get(), target: this.key } } ) ); GM_setValue( this.key, value ); } get( defaultValue = null ) { return GM_getValue( this.key, defaultValue ); } remove() { dispatchEvent( new CustomEvent( "GMStorageUpdate", { detail: { newValue: null, oldValue: this.get(), target: this.key } } ) ); GM_deleteValue( this.key ); } } const globalConfigStorage = function uploadConfig() { let config = { opacity: new GMStorage( "opacity" ), displayBtn: new GMStorage( "displayBtn" ), getElementDelayPerSecond: new GMStorage( "getElementDelayPerSecond" ) }; config.opacity.set( globalConfig.opacity ); config.displayBtn.set( globalConfig.displayBtn ); config.getElementDelayPerSecond.set( globalConfig.getElementDelayPerSecond ); return config; }(); const readStyle = { readBtnSign: { ".is-read": { transition: "opacity .25s ease-in-out", opacity: globalConfigStorage.opacity.get() }, ".is-read:hover": { opacity: 1 }, ".is-read, .is-not-read": { position: "relative" }, ".is-read::before": { content: '"已看"', "background-color": "hsla(0, 0%, 60%, .77)", "font-size": "13px", padding: "3px 8px", color: "#fff", "font-weight": 700, "border-radius": "4px", "z-index": 1, display: globalConfigStorage.displayBtn.get() ? "block" : "none" }, ".is-not-read::before": { content: '"未看"', "background-color": "rgba(3, 169, 244, .77)", "font-size": "13px", padding: "3px 8px", color: "#fff", "font-weight": 700, "border-radius": "4px", "z-index": 1, display: globalConfigStorage.displayBtn.get() ? "block" : "none" }, [`.post-list .is-read::before, .post-list .is-not-read::before,\n\t\t\t.category-post .is-read::before, .category-post .is-not-read::before,\n\t\t\t.rank-post .is-read::before, .rank-post .is-not-read::before `]: { position: "absolute", top: "5px", left: "calc(15px + 5px)" }, ".related-post .is-read::before, .related-post .is-not-read::before": { position: "absolute", top: "5px", left: "calc(6px + 5px)" }, ".card-body .is-read::before, .card-body .is-not-read::before": { position: "absolute", top: "5px", right: "5px" } } }; function transformedPrefSelectorList( prefSelectorList ) { return Object.entries( Object.values( prefSelectorList ).flat().reduce( ( ( result, current ) => ( { ...result, ...current } ) ) ) ).map( ( ( [ selector, rule ] ) => ( { selector: selector, rule: rule } ) ) ); } function addReadStyle( cssRule2 ) { cssRule2.pushList( transformedPrefSelectorList( readStyle ) ).submit(); } class Info { constructor( projectName ) { __publicField( this, "projectName" ); __publicField( this, "header" ); this.projectName = projectName; this.header = `[${ projectName }]`; } log( ...msg ) { ( () => {} )( ...this.contentInfo( ...msg ) ); } info( ...msg ) { console.info( ...this.contentInfo( ...msg ) ); } warn( ...msg ) { console.warn( ...this.contentInfo( ...msg ) ); } error( ...msg ) { console.error( ...this.contentInfo( ...msg ) ); } contentInfo( ...msg ) { return [ this.header, ...msg ]; } } class EntryBranch { constructor() { __publicField( this, "branchList", [] ); } add( condition, callback ) { if ( typeof condition === "boolean" ) { this.branchList.push( [ () => condition, callback ] ); } else { this.branchList.push( [ condition, callback ] ); } return this; } default( callback ) { this.add( true, callback ); return this; } run() { const entry = this.branchList.find( ( entry2 => entry2[0]() ) ); if ( entry ) { new Info( "EntryBranch" ).log( "进入分支", entry ); entry[1](); } } } function isMatchURL( ...regExpList ) { const matchResultList = []; regExpList.forEach( ( regExp => { if ( typeof regExp === "string" ) { regExp = new RegExp( regExp ); } matchResultList.push( !!document.URL.match( regExp ) ); } ) ); return matchResultList.includes( true ); } function freshListenerPushState( callback, s = 1 ) { let _pushState = window.history.pushState; window.history.pushState = function () { setTimeout( callback, s * 1e3 ); return _pushState.apply( this, arguments ); }; } const getEl = document.querySelector.bind( document ); const getEls = document.querySelectorAll.bind( document ); function parsePost( postList, postCard ) { const domList = { postCard: postCard, link: postCard.querySelector( ".post-card a" ) || postCard.querySelector( ".sidebar-rank-post-wrap a" ) || postCard.querySelector( ".related-post-card a" ) }; if ( !domList.postCard.innerText ) { return; } postList.push( { target: postCard, achieveId: Number( domList.link.href.slice( domList.link.href.lastIndexOf( "/" ) + 1 ) ) } ); } function writeHistoryBtn( postList ) { for ( const post of postList ) { if ( post.isRead ) { print.log( post.achieveId, "is-read" ); post.target.classList.remove( "is-not-read" ); post.target.classList.add( "is-read" ); } else { print.log( post.achieveId, "is-not-read" ); post.target.classList.remove( "is-read" ); post.target.classList.add( "is-not-read" ); } } } function bindNotReadBtnEvent( postList ) { postList.forEach( ( post => { if ( !post.isRead ) { let bindPostClickEvent = function ( e ) { if ( e.button !== 2 ) { post.target.classList.remove( "is-not-read" ); post.target.classList.add( "is-read" ); } else { post.target.addEventListener( "mousedown", bindPostClickEvent, { once: true } ); } }; post.target.addEventListener( "mousedown", bindPostClickEvent, { once: true } ); } } ) ); } async function getReadHistory( postList ) { const postDataList = []; for ( const post of postList ) { postDataList.push( { ...post, isRead: await readHistory.has( post.achieveId ) } ); } return postDataList; } async function parseMainPost() { const postList = []; const postCardList = getEls( "span.row .post" ); postCardList == null ? void 0 : postCardList.forEach( ( post => { parsePost( postList, post ); } ) ); const postDataList = await getReadHistory( postList ); writeHistoryBtn( postDataList ); bindNotReadBtnEvent( postDataList ); } async function parseAsidePost() { const postList = []; const postCardList = getEls( ".sidebar-rank-post-card li" ); postCardList == null ? void 0 : postCardList.forEach( ( post => { parsePost( postList, post ); } ) ); const postDataList = await getReadHistory( postList ); writeHistoryBtn( postDataList ); } function getElement( parent = document.body, selector, timeoutPerSecond = 0, getElementDelayPerSecond = 0 ) { return new Promise( ( resolve => { let result = parent.querySelector( selector ); if ( result ) { return resolve( result ); } let timer; const mutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver; if ( mutationObserver ) { const observer = new mutationObserver( ( mutations => { for ( let mutation of mutations ) { for ( let addedNode of mutation.addedNodes ) { if ( addedNode instanceof Element ) { result = addedNode.matches( selector ) ? addedNode : addedNode.querySelector( selector ); if ( result ) { observer.disconnect(); timer && clearTimeout( timer ); setTimeout( ( () => resolve( result ) ), getElementDelayPerSecond * 1e3 ); } } } } } ) ); observer.observe( parent, { childList: true, subtree: true } ); if ( timeoutPerSecond > 0 ) { timer = setTimeout( ( () => { observer.disconnect(); return resolve( null ); } ), timeoutPerSecond * 1e3 ); } } } ) ); } async function HomePageEntry() { var _a; await getElement( document.body, ".post-card-wrap .cover", 20, globalConfig.getElementDelayPerSecond ); await parseMainPost(); await getElement( document.body, ".sidebar-rank-post-wrap .cover", 20, globalConfig.getElementDelayPerSecond ); await parseAsidePost(); ( _a = getEl( ".nav-pills" ) ) == null ? void 0 : _a.addEventListener( "click", ( () => { setTimeout( ( () => { HomePageEntry(); } ), 500 ); } ), { once: true } ); } function promisifyRequest( request ) { return new Promise( ( ( resolve, reject ) => { request.oncomplete = request.onsuccess = () => resolve( request.result ); request.onabort = request.onerror = () => reject( request.error ); } ) ); } function createStore( dbName, storeName ) { const request = indexedDB.open( dbName ); request.onupgradeneeded = () => request.result.createObjectStore( storeName ); const dbp = promisifyRequest( request ); return ( txMode, callback ) => dbp.then( ( db => callback( db.transaction( storeName, txMode ).objectStore( storeName ) ) ) ); } let defaultGetStoreFunc; function defaultGetStore() { if ( !defaultGetStoreFunc ) { defaultGetStoreFunc = createStore( "keyval-store", "keyval" ); } return defaultGetStoreFunc; } function get( key, customStore = defaultGetStore() ) { return customStore( "readonly", ( store => promisifyRequest( store.get( key ) ) ) ); } function setMany( entries, customStore = defaultGetStore() ) { return customStore( "readwrite", ( store => { entries.forEach( ( entry => store.put( entry[1], entry[0] ) ) ); return promisifyRequest( store.transaction ); } ) ); } function eachCursor( store, callback ) { store.openCursor().onsuccess = function () { if ( !this.result ) return; callback( this.result ); this.result.continue(); }; return promisifyRequest( store.transaction ); } function values( customStore = defaultGetStore() ) { return customStore( "readonly", ( store => { if ( store.getAll ) { return promisifyRequest( store.getAll() ); } const items = []; return eachCursor( store, ( cursor => items.push( cursor.value ) ) ).then( ( () => items ) ); } ) ); } class ReadHistory { constructor() { __publicField( this, "store" ); this.store = createStore( "animax-post", "post-view-history" ); } async has( key ) { return !!( await get( key, this.store ) ); } getAll() { return values( this.store ); } async setList( historyList ) { const entries = []; historyList.forEach( ( history => { entries.push( [ void 0, history ] ); } ) ); await setMany( entries, this.store ); } } async function categoryPageEntry() { await getElement( document.body, ".post-card-wrap .cover", 20, globalConfig.getElementDelayPerSecond ); await parseMainPost(); } class CSSRule { constructor() { __publicField( this, "cssRuleSet", new Set ); __publicField( this, "styleDom", document.createElement( "style" ) ); } push( selector, rule ) { let ruleString = ""; for ( let ruleKey in rule ) { const ruleValue = rule[ruleKey]; ruleString += `${ ruleKey }:${ ruleValue };`; } this.cssRuleSet.add( `${ selector } {${ ruleString }}` ); return this; } pushImportant( selector, rule ) { let ruleString = ""; for ( let ruleKey in rule ) { let ruleValue = rule[ruleKey]; if ( typeof ruleValue === "string" ) { ruleValue = ruleValue.replace( "!important", "" ); } ruleString += `${ ruleKey }:${ ruleValue } !important;`; } this.cssRuleSet.add( `${ selector } {${ ruleString }}` ); return this; } pushHide( selector ) { this.pushImportant( selector, { display: "none" } ); return this; } pushHideList( selectorList ) { selectorList.forEach( ( selector => { this.pushImportant( selector, { display: "none" } ); } ) ); return this; } pushList( ruleList ) { ruleList.forEach( ( ( { selector: selector, rule: rule } ) => { this.push( selector, rule ); } ) ); return this; } pushImportantList( ruleList ) { ruleList.forEach( ( ( { selector: selector, rule: rule } ) => { this.pushImportant( selector, rule ); } ) ); return this; } submit() { this.removeAll(); new Info( "AddStyle" ).log( Array.from( this.cssRuleSet ).join( " " ) ); this.styleDom = GM_addStyle( Array.from( this.cssRuleSet ).join( " " ) ); } removeAll() { if ( this.styleDom ) { this.styleDom.remove(); } return this; } } async function rankPageEntry() { var _a; await getElement( document.body, ".post-card-content > .cover", 20, globalConfig.getElementDelayPerSecond ); await parseMainPost(); ( _a = getEl( ".rank-wrapper" ) ) == null ? void 0 : _a.addEventListener( "click", ( () => { setTimeout( rankPageEntry, 500 ); } ), { once: true } ); } async function parseAchieveBottomPost() { const postList = []; const postCardList = getEls( ".related-post .related-post-card" ); postCardList == null ? void 0 : postCardList.forEach( ( post => { parsePost( postList, post ); } ) ); const postDataList = await getReadHistory( postList ); writeHistoryBtn( postDataList ); } async function achievePageEntry() { var _a; await getElement( document.body, ".related-post-card .cover", 20, globalConfig.getElementDelayPerSecond ); await parseAchieveBottomPost(); await getElement( document.body, ".sidebar-rank-post-wrap .cover", 20, globalConfig.getElementDelayPerSecond ); await parseAsidePost(); ( _a = getEl( ".nav-pills" ) ) == null ? void 0 : _a.addEventListener( "click", ( () => { setTimeout( ( () => { achievePageEntry(); } ), 500 ); } ), { once: true } ); } async function historyBackup() { const historyList = await readHistory.getAll(); const GMHistory = new GMStorage( "history" ); const GMHistoryList = GMHistory.get( [] ); print.info( "正在比较备份记录条目: ", `脚本储存条目(${ GMHistoryList.length })`, `仓库本地储存条目(${ historyList.length })` ); if ( GMHistoryList.length > historyList.length ) { print.info( `历史记录丢失, 正在将备份写入历史记录: `, GMHistoryList ); await readHistory.setList( GMHistoryList ); } else if ( GMHistoryList.length < historyList.length ) { print.info( "正在备份历史记录到脚本储存中. " ); GMHistory.set( [ ...new Set( [ ...GMHistoryList, ...historyList ] ) ] ); } } async function pushNewReadAchieve() { const achieveId = document.URL.slice( document.URL.lastIndexOf( "/" ) + 1 ); if ( await readHistory.has( Number( achieveId ) ) ) { localStorage.setItem( "history", achieveId ); } } const downloadFile = ( blob, fileName ) => { const url = URL.createObjectURL( blob ); const a = document.createElement( "a" ); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL( url ); a.remove(); }; function importJson() { const importInput = GM_addElement( "input", { type: "file", accept: ".json" } ); importInput.click(); return new Promise( ( resolve => { importInput.addEventListener( "change", ( e => { const inputDom = e.target; if ( inputDom.files ) { const fileReader = new FileReader; fileReader.readAsText( inputDom.files[0] ); fileReader.onload = () => { resolve( fileReader.result || "" ); }; } } ) ); } ) ); } class SaveBtn { constructor() { __publicField( this, "domList", { ulList: getEl( ".menu-group-list" ) } ); } download() { var _a; const downloadBtn = GM_addElement( this.domList.ulList, "li", { class: "menu-list-item" } ); downloadBtn.innerHTML = `\n\t\t\t<a class="router-link-exact-active download-btn" href="javascript:;">保存历史记录</a>\n\t\t`; ( _a = downloadBtn.querySelector( ".download-btn" ) ) == null ? void 0 : _a.addEventListener( "click", ( async () => { downloadFile( new Blob( [ JSON.stringify( await readHistory.getAll() ) ], { type: "application/json" } ), "ReadHistoryBackUp.json" ); } ) ); } import() { var _a; const downloadBtn = GM_addElement( this.domList.ulList, "li", { class: "menu-list-item" } ); downloadBtn.innerHTML = `\n\t\t\t<a class="router-link-exact-active import-btn" href="javascript:;">导入历史记录</a>\n\t\t`; ( _a = downloadBtn.querySelector( ".import-btn" ) ) == null ? void 0 : _a.addEventListener( "click", ( async () => { const historyJsonString = await importJson(); const historyArray = JSON.parse( historyJsonString ); if ( Array.isArray( historyArray ) ) { const isRightHistoryArray = !historyArray.find( ( ( { id: id, date: date } ) => !( id && date ) ) ); if ( isRightHistoryArray ) { await readHistory.setList( historyArray ); alert( "导入历史记录成功. " ); return; } } else { alert( "导入Json文件为错误的历史记录文件. " ); } } ) ); } } async function addFreshBtn( entryBranch ) { var _a; await getElement( document.body, ".navbar-nav", 0, .5 ); const freshBtn = GM_addElement( getEl( ".navbar-nav" ), "li", { class: "nav-item" } ); const dataHash = getEl( ".navbar-nav > .nav-item" ).getAttributeNames().find( ( attribute => attribute.startsWith( "data-v" ) ) ); freshBtn.innerHTML = `\n\t\t<a class="text-truncate fresh-btn" ${ dataHash } href="javascript:;">刷新</a>\n\t`; ( _a = freshBtn.querySelector( ".fresh-btn" ) ) == null ? void 0 : _a.addEventListener( "click", ( () => { entryBranch.run(); } ) ); } const print = new Info( "postCardList" ); const cssRule = new CSSRule; const readHistory = new ReadHistory; ( async () => { addReadStyle( cssRule ); await historyBackup(); const entryBranch = new EntryBranch; entryBranch.add( ( () => isMatchURL( "^https://cangku.moe/$" ) ), HomePageEntry ).add( ( () => isMatchURL( "^https://cangku.moe/\\?page" ) ), ( async () => { await HomePageEntry(); cssRule.pushHide( ".global-announce" ).submit(); } ) ).add( ( () => isMatchURL( /https:\/\/cangku.moe\/archives\/\d+/ ) ), ( async () => { await achievePageEntry(); await pushNewReadAchieve(); } ) ).add( ( () => isMatchURL( /https:\/\/cangku.moe\/category\/\d+/ ) ), categoryPageEntry ).add( ( () => isMatchURL( "https://cangku.moe/rank" ) ), rankPageEntry ).add( ( () => isMatchURL( "https://cangku.moe/account/" ) ), ( async () => { await getElement( document.body, ".menu-group-list", 0, .5 ); const saveBtn = new SaveBtn; saveBtn.download(); saveBtn.import(); } ) ).run(); freshListenerPushState( ( () => { entryBranch.run(); } ) ); await addFreshBtn( entryBranch ); } )();